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)
163 lines
4.2 KiB
TypeScript
163 lines
4.2 KiB
TypeScript
interface Offer {
|
|
id: string;
|
|
buyerId: string;
|
|
buyerName?: string;
|
|
amount: number;
|
|
message?: string;
|
|
status: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface OfferListProps {
|
|
offers: Offer[];
|
|
isSellerView?: boolean;
|
|
onAccept?: (offerId: string) => void;
|
|
onReject?: (offerId: string) => void;
|
|
onWithdraw?: (offerId: string) => void;
|
|
}
|
|
|
|
const statusColors: Record<string, string> = {
|
|
pending: 'bg-yellow-100 text-yellow-800',
|
|
accepted: 'bg-green-100 text-green-800',
|
|
rejected: 'bg-red-100 text-red-800',
|
|
withdrawn: 'bg-gray-100 text-gray-800',
|
|
expired: 'bg-gray-100 text-gray-800',
|
|
};
|
|
|
|
const statusLabels: Record<string, string> = {
|
|
pending: 'Pending',
|
|
accepted: 'Accepted',
|
|
rejected: 'Rejected',
|
|
withdrawn: 'Withdrawn',
|
|
expired: 'Expired',
|
|
};
|
|
|
|
export function OfferList({
|
|
offers,
|
|
isSellerView = false,
|
|
onAccept,
|
|
onReject,
|
|
onWithdraw,
|
|
}: OfferListProps) {
|
|
if (offers.length === 0) {
|
|
return (
|
|
<div className="text-center py-8 bg-gray-50 rounded-lg">
|
|
<div className="text-3xl mb-2">📭</div>
|
|
<p className="text-gray-500">No offers yet</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{offers.map((offer) => (
|
|
<OfferItem
|
|
key={offer.id}
|
|
offer={offer}
|
|
isSellerView={isSellerView}
|
|
onAccept={onAccept}
|
|
onReject={onReject}
|
|
onWithdraw={onWithdraw}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface OfferItemProps {
|
|
offer: Offer;
|
|
isSellerView: boolean;
|
|
onAccept?: (offerId: string) => void;
|
|
onReject?: (offerId: string) => void;
|
|
onWithdraw?: (offerId: string) => void;
|
|
}
|
|
|
|
function OfferItem({
|
|
offer,
|
|
isSellerView,
|
|
onAccept,
|
|
onReject,
|
|
onWithdraw,
|
|
}: OfferItemProps) {
|
|
const isPending = offer.status === 'pending';
|
|
|
|
return (
|
|
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
{isSellerView && (
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
|
<span>👤</span>
|
|
</div>
|
|
<span className="font-medium text-gray-900">
|
|
{offer.buyerName || 'Anonymous'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div className="text-2xl font-bold text-green-600">
|
|
${offer.amount.toFixed(2)}
|
|
</div>
|
|
<div className="text-sm text-gray-500 mt-1">
|
|
{new Date(offer.createdAt).toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
})}
|
|
</div>
|
|
</div>
|
|
<span
|
|
className={`px-3 py-1 rounded-full text-sm font-medium ${
|
|
statusColors[offer.status]
|
|
}`}
|
|
>
|
|
{statusLabels[offer.status]}
|
|
</span>
|
|
</div>
|
|
|
|
{offer.message && (
|
|
<div className="mt-3 p-3 bg-gray-50 rounded-lg text-gray-700 text-sm">
|
|
"{offer.message}"
|
|
</div>
|
|
)}
|
|
|
|
{isPending && (
|
|
<div className="flex gap-2 mt-4 pt-4 border-t">
|
|
{isSellerView ? (
|
|
<>
|
|
{onAccept && (
|
|
<button
|
|
onClick={() => onAccept(offer.id)}
|
|
className="flex-1 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition font-medium"
|
|
>
|
|
Accept
|
|
</button>
|
|
)}
|
|
{onReject && (
|
|
<button
|
|
onClick={() => onReject(offer.id)}
|
|
className="flex-1 py-2 border border-red-300 text-red-600 rounded-lg hover:bg-red-50 transition font-medium"
|
|
>
|
|
Reject
|
|
</button>
|
|
)}
|
|
</>
|
|
) : (
|
|
onWithdraw && (
|
|
<button
|
|
onClick={() => onWithdraw(offer.id)}
|
|
className="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition"
|
|
>
|
|
Withdraw Offer
|
|
</button>
|
|
)
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default OfferList;
|