localgreenchain/pages/marketplace/create.tsx
Claude b3c2af51bf
Implement marketplace foundation (Agent 9)
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)
2025-11-23 03:58:08 +00:00

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>
);
}