localgreenchain/components/marketplace/ListingCard.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

149 lines
5.2 KiB
TypeScript

import Link from 'next/link';
interface Listing {
id: string;
title: string;
description: string;
price: number;
currency: string;
quantity: number;
category: string;
sellerName?: string;
location?: { city?: string; region?: string };
tags: string[];
viewCount: number;
}
const categoryLabels: Record<string, string> = {
seeds: 'Seeds',
seedlings: 'Seedlings',
mature_plants: 'Mature Plants',
cuttings: 'Cuttings',
produce: 'Produce',
supplies: 'Supplies',
};
const categoryIcons: Record<string, string> = {
seeds: '🌰',
seedlings: '🌱',
mature_plants: '🪴',
cuttings: '✂️',
produce: '🥬',
supplies: '🧰',
};
interface ListingCardProps {
listing: Listing;
variant?: 'default' | 'compact' | 'featured';
}
export function ListingCard({ listing, variant = 'default' }: ListingCardProps) {
if (variant === 'compact') {
return (
<Link href={`/marketplace/listings/${listing.id}`}>
<a className="flex items-center gap-4 p-4 bg-white rounded-lg shadow hover:shadow-md transition border border-gray-200">
<div className="w-16 h-16 bg-gradient-to-br from-green-100 to-emerald-100 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-2xl">{categoryIcons[listing.category] || '🌿'}</span>
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 truncate">{listing.title}</h3>
<p className="text-sm text-gray-500">{categoryLabels[listing.category]}</p>
</div>
<div className="text-right">
<div className="font-bold text-green-600">${listing.price.toFixed(2)}</div>
<div className="text-xs text-gray-500">{listing.quantity} avail.</div>
</div>
</a>
</Link>
);
}
if (variant === 'featured') {
return (
<Link href={`/marketplace/listings/${listing.id}`}>
<a className="block bg-white rounded-xl shadow-lg hover:shadow-xl transition overflow-hidden border-2 border-green-200">
<div className="relative">
<div className="h-56 bg-gradient-to-br from-green-100 to-emerald-100 flex items-center justify-center">
<span className="text-7xl">{categoryIcons[listing.category] || '🌿'}</span>
</div>
<div className="absolute top-4 left-4">
<span className="px-3 py-1 bg-green-600 text-white text-sm font-medium rounded-full">
Featured
</span>
</div>
</div>
<div className="p-6">
<div className="flex justify-between items-start mb-3">
<h3 className="text-xl font-bold text-gray-900 line-clamp-1">
{listing.title}
</h3>
<span className="text-2xl font-bold text-green-600">
${listing.price.toFixed(2)}
</span>
</div>
<p className="text-gray-600 line-clamp-2 mb-4">
{listing.description}
</p>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2 text-sm text-gray-500">
<span>{categoryLabels[listing.category]}</span>
<span></span>
<span>{listing.quantity} available</span>
</div>
<span className="text-sm text-gray-400">
{listing.viewCount} views
</span>
</div>
</div>
</a>
</Link>
);
}
// Default variant
return (
<Link href={`/marketplace/listings/${listing.id}`}>
<a className="block bg-white rounded-lg shadow hover:shadow-lg transition overflow-hidden border border-gray-200">
<div className="h-48 bg-gradient-to-br from-green-100 to-emerald-100 flex items-center justify-center">
<span className="text-6xl">{categoryIcons[listing.category] || '🌿'}</span>
</div>
<div className="p-4">
<div className="flex justify-between items-start mb-2">
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1">
{listing.title}
</h3>
<span className="text-lg font-bold text-green-600">
${listing.price.toFixed(2)}
</span>
</div>
<p className="text-gray-600 text-sm line-clamp-2 mb-3">
{listing.description}
</p>
<div className="flex justify-between items-center text-sm text-gray-500">
<span>{categoryLabels[listing.category]}</span>
<span>{listing.quantity} available</span>
</div>
{listing.sellerName && (
<div className="mt-2 text-sm text-gray-500">
by {listing.sellerName}
</div>
)}
{listing.tags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{listing.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full"
>
{tag}
</span>
))}
</div>
)}
</div>
</a>
</Link>
);
}
export default ListingCard;