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)
273 lines
8.7 KiB
TypeScript
273 lines
8.7 KiB
TypeScript
// In-Memory Store for Marketplace
|
|
// This will be replaced with Prisma database calls once Agent 2 completes database setup
|
|
|
|
import {
|
|
Listing,
|
|
Offer,
|
|
SellerProfile,
|
|
WishlistItem,
|
|
ListingCategory,
|
|
ListingStatus,
|
|
OfferStatus,
|
|
} from './types';
|
|
|
|
// In-memory storage
|
|
const listings: Map<string, Listing> = new Map();
|
|
const offers: Map<string, Offer> = new Map();
|
|
const sellerProfiles: Map<string, SellerProfile> = new Map();
|
|
const wishlistItems: Map<string, WishlistItem> = new Map();
|
|
|
|
// Helper to generate IDs
|
|
export const generateId = (): string => {
|
|
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
};
|
|
|
|
// Seed with sample data for development
|
|
const seedSampleData = () => {
|
|
const sampleListings: Listing[] = [
|
|
{
|
|
id: 'listing-1',
|
|
sellerId: 'user-1',
|
|
sellerName: 'Green Thumb Gardens',
|
|
title: 'Heirloom Tomato Seedlings - Cherokee Purple',
|
|
description: 'Beautiful heirloom Cherokee Purple tomato seedlings, organically grown. These produce large, deep purple-red fruits with rich, complex flavor. Perfect for home gardens.',
|
|
price: 4.99,
|
|
currency: 'USD',
|
|
quantity: 24,
|
|
category: ListingCategory.SEEDLINGS,
|
|
status: ListingStatus.ACTIVE,
|
|
location: { lat: 40.7128, lng: -74.006, city: 'New York', region: 'NY' },
|
|
tags: ['organic', 'heirloom', 'tomato', 'vegetable'],
|
|
images: [
|
|
{ id: 'img-1', listingId: 'listing-1', url: '/images/tomato-seedling.jpg', alt: 'Cherokee Purple Tomato Seedling', isPrimary: true, createdAt: new Date() }
|
|
],
|
|
viewCount: 142,
|
|
createdAt: new Date('2024-03-01'),
|
|
updatedAt: new Date('2024-03-15'),
|
|
},
|
|
{
|
|
id: 'listing-2',
|
|
sellerId: 'user-2',
|
|
sellerName: 'Urban Herb Farm',
|
|
title: 'Fresh Basil Plants - Genovese',
|
|
description: 'Ready-to-harvest Genovese basil plants grown in our vertical farm. Perfect for pesto, salads, and Italian cuisine. Each plant is 6-8 inches tall.',
|
|
price: 6.50,
|
|
currency: 'USD',
|
|
quantity: 50,
|
|
category: ListingCategory.MATURE_PLANTS,
|
|
status: ListingStatus.ACTIVE,
|
|
location: { lat: 34.0522, lng: -118.2437, city: 'Los Angeles', region: 'CA' },
|
|
tags: ['herbs', 'basil', 'culinary', 'fresh'],
|
|
images: [],
|
|
viewCount: 89,
|
|
createdAt: new Date('2024-03-10'),
|
|
updatedAt: new Date('2024-03-10'),
|
|
},
|
|
{
|
|
id: 'listing-3',
|
|
sellerId: 'user-1',
|
|
sellerName: 'Green Thumb Gardens',
|
|
title: 'Organic Lettuce Mix Seeds',
|
|
description: 'Premium mix of organic lettuce seeds including romaine, butterhead, and red leaf varieties. Perfect for succession planting.',
|
|
price: 3.99,
|
|
currency: 'USD',
|
|
quantity: 100,
|
|
category: ListingCategory.SEEDS,
|
|
status: ListingStatus.ACTIVE,
|
|
location: { lat: 40.7128, lng: -74.006, city: 'New York', region: 'NY' },
|
|
tags: ['organic', 'seeds', 'lettuce', 'salad'],
|
|
images: [],
|
|
viewCount: 256,
|
|
createdAt: new Date('2024-02-15'),
|
|
updatedAt: new Date('2024-03-01'),
|
|
},
|
|
{
|
|
id: 'listing-4',
|
|
sellerId: 'user-3',
|
|
sellerName: 'Succulent Paradise',
|
|
title: 'Assorted Succulent Cuttings - 10 Pack',
|
|
description: 'Beautiful assortment of succulent cuttings ready for propagation. Includes echeveria, sedum, and crassula varieties.',
|
|
price: 15.00,
|
|
currency: 'USD',
|
|
quantity: 30,
|
|
category: ListingCategory.CUTTINGS,
|
|
status: ListingStatus.ACTIVE,
|
|
location: { lat: 33.4484, lng: -112.074, city: 'Phoenix', region: 'AZ' },
|
|
tags: ['succulents', 'cuttings', 'propagation', 'drought-tolerant'],
|
|
images: [],
|
|
viewCount: 178,
|
|
createdAt: new Date('2024-03-05'),
|
|
updatedAt: new Date('2024-03-12'),
|
|
},
|
|
{
|
|
id: 'listing-5',
|
|
sellerId: 'user-2',
|
|
sellerName: 'Urban Herb Farm',
|
|
title: 'Fresh Microgreens - Chef\'s Mix',
|
|
description: 'Freshly harvested microgreens mix including sunflower, radish, and pea shoots. Harvested same day as shipping for maximum freshness.',
|
|
price: 8.99,
|
|
currency: 'USD',
|
|
quantity: 40,
|
|
category: ListingCategory.PRODUCE,
|
|
status: ListingStatus.ACTIVE,
|
|
location: { lat: 34.0522, lng: -118.2437, city: 'Los Angeles', region: 'CA' },
|
|
tags: ['microgreens', 'fresh', 'produce', 'chef'],
|
|
images: [],
|
|
viewCount: 312,
|
|
createdAt: new Date('2024-03-18'),
|
|
updatedAt: new Date('2024-03-18'),
|
|
},
|
|
];
|
|
|
|
const sampleProfiles: SellerProfile[] = [
|
|
{
|
|
userId: 'user-1',
|
|
displayName: 'Green Thumb Gardens',
|
|
bio: 'Family-owned nursery specializing in heirloom vegetables and native plants.',
|
|
location: { city: 'New York', region: 'NY' },
|
|
rating: 4.8,
|
|
reviewCount: 127,
|
|
totalSales: 523,
|
|
memberSince: new Date('2023-01-15'),
|
|
verified: true,
|
|
},
|
|
{
|
|
userId: 'user-2',
|
|
displayName: 'Urban Herb Farm',
|
|
bio: 'Vertical farm growing fresh herbs and microgreens in the heart of LA.',
|
|
location: { city: 'Los Angeles', region: 'CA' },
|
|
rating: 4.9,
|
|
reviewCount: 89,
|
|
totalSales: 412,
|
|
memberSince: new Date('2023-03-20'),
|
|
verified: true,
|
|
},
|
|
{
|
|
userId: 'user-3',
|
|
displayName: 'Succulent Paradise',
|
|
bio: 'Desert plant enthusiast sharing the beauty of succulents.',
|
|
location: { city: 'Phoenix', region: 'AZ' },
|
|
rating: 4.7,
|
|
reviewCount: 56,
|
|
totalSales: 198,
|
|
memberSince: new Date('2023-06-01'),
|
|
verified: false,
|
|
},
|
|
];
|
|
|
|
// Seed listings
|
|
sampleListings.forEach(listing => {
|
|
listings.set(listing.id, listing);
|
|
});
|
|
|
|
// Seed profiles
|
|
sampleProfiles.forEach(profile => {
|
|
sellerProfiles.set(profile.userId, profile);
|
|
});
|
|
};
|
|
|
|
// Initialize with sample data
|
|
seedSampleData();
|
|
|
|
// Export store operations
|
|
export const listingStore = {
|
|
getAll: (): Listing[] => Array.from(listings.values()),
|
|
|
|
getById: (id: string): Listing | undefined => listings.get(id),
|
|
|
|
getBySellerId: (sellerId: string): Listing[] =>
|
|
Array.from(listings.values()).filter(l => l.sellerId === sellerId),
|
|
|
|
create: (listing: Listing): Listing => {
|
|
listings.set(listing.id, listing);
|
|
return listing;
|
|
},
|
|
|
|
update: (id: string, updates: Partial<Listing>): Listing | undefined => {
|
|
const existing = listings.get(id);
|
|
if (!existing) return undefined;
|
|
const updated = { ...existing, ...updates, updatedAt: new Date() };
|
|
listings.set(id, updated);
|
|
return updated;
|
|
},
|
|
|
|
delete: (id: string): boolean => listings.delete(id),
|
|
|
|
incrementViewCount: (id: string): void => {
|
|
const listing = listings.get(id);
|
|
if (listing) {
|
|
listing.viewCount++;
|
|
listings.set(id, listing);
|
|
}
|
|
},
|
|
};
|
|
|
|
export const offerStore = {
|
|
getAll: (): Offer[] => Array.from(offers.values()),
|
|
|
|
getById: (id: string): Offer | undefined => offers.get(id),
|
|
|
|
getByListingId: (listingId: string): Offer[] =>
|
|
Array.from(offers.values()).filter(o => o.listingId === listingId),
|
|
|
|
getByBuyerId: (buyerId: string): Offer[] =>
|
|
Array.from(offers.values()).filter(o => o.buyerId === buyerId),
|
|
|
|
create: (offer: Offer): Offer => {
|
|
offers.set(offer.id, offer);
|
|
return offer;
|
|
},
|
|
|
|
update: (id: string, updates: Partial<Offer>): Offer | undefined => {
|
|
const existing = offers.get(id);
|
|
if (!existing) return undefined;
|
|
const updated = { ...existing, ...updates, updatedAt: new Date() };
|
|
offers.set(id, updated);
|
|
return updated;
|
|
},
|
|
|
|
delete: (id: string): boolean => offers.delete(id),
|
|
};
|
|
|
|
export const sellerProfileStore = {
|
|
getByUserId: (userId: string): SellerProfile | undefined =>
|
|
sellerProfiles.get(userId),
|
|
|
|
create: (profile: SellerProfile): SellerProfile => {
|
|
sellerProfiles.set(profile.userId, profile);
|
|
return profile;
|
|
},
|
|
|
|
update: (userId: string, updates: Partial<SellerProfile>): SellerProfile | undefined => {
|
|
const existing = sellerProfiles.get(userId);
|
|
if (!existing) return undefined;
|
|
const updated = { ...existing, ...updates };
|
|
sellerProfiles.set(userId, updated);
|
|
return updated;
|
|
},
|
|
};
|
|
|
|
export const wishlistStore = {
|
|
getByUserId: (userId: string): WishlistItem[] =>
|
|
Array.from(wishlistItems.values()).filter(w => w.userId === userId),
|
|
|
|
add: (item: WishlistItem): WishlistItem => {
|
|
wishlistItems.set(item.id, item);
|
|
return item;
|
|
},
|
|
|
|
remove: (userId: string, listingId: string): boolean => {
|
|
const item = Array.from(wishlistItems.values()).find(
|
|
w => w.userId === userId && w.listingId === listingId
|
|
);
|
|
if (item) {
|
|
return wishlistItems.delete(item.id);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
exists: (userId: string, listingId: string): boolean =>
|
|
Array.from(wishlistItems.values()).some(
|
|
w => w.userId === userId && w.listingId === listingId
|
|
),
|
|
};
|