Add comprehensive plant trading marketplace with: - Prisma schema with marketplace models (Listing, Offer, SellerProfile, WishlistItem) - Service layer for listings, offers, search, and matching - API endpoints for CRUD operations, search, and recommendations - Marketplace pages: home, listing detail, create, my-listings, my-offers - Reusable UI components: ListingCard, ListingGrid, OfferForm, SearchFilters, etc. Features: - Browse and search listings by category, price, tags - Create and manage listings (draft, active, sold, cancelled) - Make and manage offers on listings - Seller and buyer views with statistics - Featured and recommended listings - In-memory store (ready for database migration via Agent 2)
282 lines
9.9 KiB
TypeScript
282 lines
9.9 KiB
TypeScript
import { useState } from 'react';
|
|
import Link from 'next/link';
|
|
import Head from 'next/head';
|
|
import { useRouter } from 'next/router';
|
|
|
|
const categories = [
|
|
{ value: 'seeds', label: 'Seeds', icon: '🌰' },
|
|
{ value: 'seedlings', label: 'Seedlings', icon: '🌱' },
|
|
{ value: 'mature_plants', label: 'Mature Plants', icon: '🪴' },
|
|
{ value: 'cuttings', label: 'Cuttings', icon: '✂️' },
|
|
{ value: 'produce', label: 'Produce', icon: '🥬' },
|
|
{ value: 'supplies', label: 'Supplies', icon: '🧰' },
|
|
];
|
|
|
|
export default function CreateListingPage() {
|
|
const router = useRouter();
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const [formData, setFormData] = useState({
|
|
title: '',
|
|
description: '',
|
|
price: '',
|
|
quantity: '1',
|
|
category: '',
|
|
tags: '',
|
|
city: '',
|
|
region: '',
|
|
});
|
|
|
|
const handleChange = (
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
|
) => {
|
|
const { name, value } = e.target;
|
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError('');
|
|
|
|
try {
|
|
const response = await fetch('/api/marketplace/listings', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-user-id': 'demo-user',
|
|
'x-user-name': 'Demo User',
|
|
},
|
|
body: JSON.stringify({
|
|
title: formData.title,
|
|
description: formData.description,
|
|
price: parseFloat(formData.price),
|
|
quantity: parseInt(formData.quantity, 10),
|
|
category: formData.category,
|
|
tags: formData.tags.split(',').map((t) => t.trim()).filter(Boolean),
|
|
location: formData.city || formData.region ? {
|
|
city: formData.city,
|
|
region: formData.region,
|
|
} : undefined,
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Failed to create listing');
|
|
}
|
|
|
|
// Redirect to the listing page
|
|
router.push(`/marketplace/listings/${data.listing.id}`);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Something went wrong');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
|
<Head>
|
|
<title>Create Listing - LocalGreenChain Marketplace</title>
|
|
</Head>
|
|
|
|
{/* Header */}
|
|
<header className="bg-white shadow-sm">
|
|
<div className="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
|
|
<div className="flex items-center justify-between">
|
|
<Link href="/marketplace">
|
|
<a className="text-2xl font-bold text-green-800">LocalGreenChain</a>
|
|
</Link>
|
|
<nav>
|
|
<Link href="/marketplace">
|
|
<a className="text-green-600 hover:text-green-700">Back to Marketplace</a>
|
|
</Link>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main className="max-w-3xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
|
|
<div className="bg-white rounded-lg shadow-lg p-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Create New Listing</h1>
|
|
<p className="text-gray-600 mb-8">
|
|
Fill in the details below to list your item on the marketplace.
|
|
</p>
|
|
|
|
{error && (
|
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Title */}
|
|
<div>
|
|
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Title *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="title"
|
|
name="title"
|
|
value={formData.title}
|
|
onChange={handleChange}
|
|
required
|
|
placeholder="e.g., Organic Tomato Seedlings"
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div>
|
|
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Description *
|
|
</label>
|
|
<textarea
|
|
id="description"
|
|
name="description"
|
|
value={formData.description}
|
|
onChange={handleChange}
|
|
required
|
|
rows={4}
|
|
placeholder="Describe your item in detail..."
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Category */}
|
|
<div>
|
|
<label htmlFor="category" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Category *
|
|
</label>
|
|
<select
|
|
id="category"
|
|
name="category"
|
|
value={formData.category}
|
|
onChange={handleChange}
|
|
required
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
>
|
|
<option value="">Select a category</option>
|
|
{categories.map((cat) => (
|
|
<option key={cat.value} value={cat.value}>
|
|
{cat.icon} {cat.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Price and Quantity */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label htmlFor="price" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Price (USD) *
|
|
</label>
|
|
<div className="relative">
|
|
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500">$</span>
|
|
<input
|
|
type="number"
|
|
id="price"
|
|
name="price"
|
|
value={formData.price}
|
|
onChange={handleChange}
|
|
required
|
|
min="0.01"
|
|
step="0.01"
|
|
placeholder="0.00"
|
|
className="w-full pl-8 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="quantity" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Quantity *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
id="quantity"
|
|
name="quantity"
|
|
value={formData.quantity}
|
|
onChange={handleChange}
|
|
required
|
|
min="1"
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Location */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label htmlFor="city" className="block text-sm font-medium text-gray-700 mb-1">
|
|
City
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="city"
|
|
name="city"
|
|
value={formData.city}
|
|
onChange={handleChange}
|
|
placeholder="e.g., New York"
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="region" className="block text-sm font-medium text-gray-700 mb-1">
|
|
State/Region
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="region"
|
|
name="region"
|
|
value={formData.region}
|
|
onChange={handleChange}
|
|
placeholder="e.g., NY"
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
<div>
|
|
<label htmlFor="tags" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tags
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="tags"
|
|
name="tags"
|
|
value={formData.tags}
|
|
onChange={handleChange}
|
|
placeholder="e.g., organic, heirloom, vegetable (comma separated)"
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Add tags to help buyers find your listing. Separate with commas.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Submit Button */}
|
|
<div className="pt-4">
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full py-4 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition disabled:opacity-50"
|
|
>
|
|
{loading ? 'Creating Listing...' : 'Create Listing'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<p className="mt-6 text-sm text-gray-500 text-center">
|
|
Your listing will be saved as a draft. You can publish it after reviewing.
|
|
</p>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|