localgreenchain/pages/marketplace/my-listings.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

319 lines
12 KiB
TypeScript

import { useState, useEffect } from 'react';
import Link from 'next/link';
import Head from 'next/head';
interface Listing {
id: string;
title: string;
description: string;
price: number;
currency: string;
quantity: number;
category: string;
status: string;
viewCount: number;
createdAt: string;
}
interface SellerStats {
totalListings: number;
activeListings: number;
soldListings: number;
totalViews: number;
averagePrice: number;
}
const categoryLabels: Record<string, string> = {
seeds: 'Seeds',
seedlings: 'Seedlings',
mature_plants: 'Mature Plants',
cuttings: 'Cuttings',
produce: 'Produce',
supplies: 'Supplies',
};
const statusColors: Record<string, string> = {
draft: 'bg-gray-100 text-gray-800',
active: 'bg-green-100 text-green-800',
sold: 'bg-blue-100 text-blue-800',
expired: 'bg-yellow-100 text-yellow-800',
cancelled: 'bg-red-100 text-red-800',
};
export default function MyListingsPage() {
const [listings, setListings] = useState<Listing[]>([]);
const [stats, setStats] = useState<SellerStats | null>(null);
const [loading, setLoading] = useState(true);
const [statusFilter, setStatusFilter] = useState<string>('');
useEffect(() => {
fetchMyListings();
}, [statusFilter]);
const fetchMyListings = async () => {
setLoading(true);
try {
const url = statusFilter
? `/api/marketplace/my-listings?status=${statusFilter}`
: '/api/marketplace/my-listings';
const response = await fetch(url, {
headers: {
'x-user-id': 'demo-user',
},
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch listings');
}
setListings(data.listings || []);
setStats(data.stats || null);
} catch (error) {
console.error('Error fetching listings:', error);
} finally {
setLoading(false);
}
};
const handlePublish = async (listingId: string) => {
try {
const response = await fetch(`/api/marketplace/listings/${listingId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-user-id': 'demo-user',
},
body: JSON.stringify({ status: 'active' }),
});
if (response.ok) {
fetchMyListings();
}
} catch (error) {
console.error('Error publishing listing:', error);
}
};
const handleCancel = async (listingId: string) => {
if (!confirm('Are you sure you want to cancel this listing?')) return;
try {
const response = await fetch(`/api/marketplace/listings/${listingId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-user-id': 'demo-user',
},
body: JSON.stringify({ status: 'cancelled' }),
});
if (response.ok) {
fetchMyListings();
}
} catch (error) {
console.error('Error cancelling listing:', error);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
<Head>
<title>My Listings - 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 className="flex gap-4">
<Link href="/marketplace/my-offers">
<a className="text-green-600 hover:text-green-700">My Offers</a>
</Link>
<Link href="/marketplace/create">
<a className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
New Listing
</a>
</Link>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-gray-900 mb-8">My Listings</h1>
{/* Stats Cards */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<div className="bg-white rounded-lg shadow p-4">
<div className="text-2xl font-bold text-gray-900">{stats.totalListings}</div>
<div className="text-sm text-gray-500">Total Listings</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-2xl font-bold text-green-600">{stats.activeListings}</div>
<div className="text-sm text-gray-500">Active</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-2xl font-bold text-blue-600">{stats.soldListings}</div>
<div className="text-sm text-gray-500">Sold</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-2xl font-bold text-gray-900">{stats.totalViews}</div>
<div className="text-sm text-gray-500">Total Views</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-2xl font-bold text-gray-900">${stats.averagePrice}</div>
<div className="text-sm text-gray-500">Avg. Price</div>
</div>
</div>
)}
{/* Filters */}
<div className="bg-white rounded-lg shadow p-4 mb-6">
<div className="flex gap-2">
<button
onClick={() => setStatusFilter('')}
className={`px-4 py-2 rounded-lg ${
!statusFilter ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
All
</button>
<button
onClick={() => setStatusFilter('draft')}
className={`px-4 py-2 rounded-lg ${
statusFilter === 'draft' ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Drafts
</button>
<button
onClick={() => setStatusFilter('active')}
className={`px-4 py-2 rounded-lg ${
statusFilter === 'active' ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Active
</button>
<button
onClick={() => setStatusFilter('sold')}
className={`px-4 py-2 rounded-lg ${
statusFilter === 'sold' ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Sold
</button>
</div>
</div>
{/* Listings */}
{loading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading your listings...</p>
</div>
) : listings.length === 0 ? (
<div className="bg-white rounded-lg shadow-lg p-12 text-center">
<div className="text-6xl mb-4">📦</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">No Listings Yet</h2>
<p className="text-gray-600 mb-6">
Create your first listing to start selling on the marketplace.
</p>
<Link href="/marketplace/create">
<a className="inline-block px-6 py-3 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition">
Create Listing
</a>
</Link>
</div>
) : (
<div className="bg-white rounded-lg shadow overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Listing
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Category
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Price
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Views
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{listings.map((listing) => (
<tr key={listing.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<Link href={`/marketplace/listings/${listing.id}`}>
<a className="text-gray-900 font-medium hover:text-green-600">
{listing.title}
</a>
</Link>
<div className="text-sm text-gray-500">
{new Date(listing.createdAt).toLocaleDateString()}
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-500">
{categoryLabels[listing.category]}
</td>
<td className="px-6 py-4 text-sm text-gray-900">
${listing.price.toFixed(2)}
<div className="text-xs text-gray-500">Qty: {listing.quantity}</div>
</td>
<td className="px-6 py-4">
<span className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${statusColors[listing.status]}`}>
{listing.status}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-500">
{listing.viewCount}
</td>
<td className="px-6 py-4 text-right text-sm">
<div className="flex gap-2 justify-end">
{listing.status === 'draft' && (
<button
onClick={() => handlePublish(listing.id)}
className="text-green-600 hover:text-green-700"
>
Publish
</button>
)}
{listing.status === 'active' && (
<button
onClick={() => handleCancel(listing.id)}
className="text-red-600 hover:text-red-700"
>
Cancel
</button>
)}
<Link href={`/marketplace/listings/${listing.id}`}>
<a className="text-gray-600 hover:text-gray-700">View</a>
</Link>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
</div>
);
}