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

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;