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)
122 lines
3.4 KiB
TypeScript
122 lines
3.4 KiB
TypeScript
// API: Marketplace Listings
|
|
// GET /api/marketplace/listings - List all active listings
|
|
// POST /api/marketplace/listings - Create a new listing
|
|
|
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
import { listingService, searchService } from '@/lib/marketplace';
|
|
import type { CreateListingInput, SearchFilters, ListingCategory } from '@/lib/marketplace/types';
|
|
|
|
export default async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse
|
|
) {
|
|
try {
|
|
switch (req.method) {
|
|
case 'GET':
|
|
return handleGet(req, res);
|
|
case 'POST':
|
|
return handlePost(req, res);
|
|
default:
|
|
res.setHeader('Allow', ['GET', 'POST']);
|
|
return res.status(405).json({ error: `Method ${req.method} Not Allowed` });
|
|
}
|
|
} catch (error) {
|
|
console.error('Marketplace listings API error:', error);
|
|
return res.status(500).json({
|
|
error: 'Internal server error',
|
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
}
|
|
}
|
|
|
|
async function handleGet(req: NextApiRequest, res: NextApiResponse) {
|
|
const {
|
|
query,
|
|
category,
|
|
minPrice,
|
|
maxPrice,
|
|
sellerId,
|
|
tags,
|
|
sortBy,
|
|
page,
|
|
limit,
|
|
} = req.query;
|
|
|
|
const filters: SearchFilters = {};
|
|
|
|
if (query && typeof query === 'string') {
|
|
filters.query = query;
|
|
}
|
|
|
|
if (category && typeof category === 'string') {
|
|
filters.category = category as ListingCategory;
|
|
}
|
|
|
|
if (minPrice && typeof minPrice === 'string') {
|
|
filters.minPrice = parseFloat(minPrice);
|
|
}
|
|
|
|
if (maxPrice && typeof maxPrice === 'string') {
|
|
filters.maxPrice = parseFloat(maxPrice);
|
|
}
|
|
|
|
if (sellerId && typeof sellerId === 'string') {
|
|
filters.sellerId = sellerId;
|
|
}
|
|
|
|
if (tags) {
|
|
const tagArray = Array.isArray(tags) ? tags : [tags];
|
|
filters.tags = tagArray.filter((t): t is string => typeof t === 'string');
|
|
}
|
|
|
|
if (sortBy && typeof sortBy === 'string') {
|
|
filters.sortBy = sortBy as SearchFilters['sortBy'];
|
|
}
|
|
|
|
if (page && typeof page === 'string') {
|
|
filters.page = parseInt(page, 10);
|
|
}
|
|
|
|
if (limit && typeof limit === 'string') {
|
|
filters.limit = parseInt(limit, 10);
|
|
}
|
|
|
|
const results = await searchService.searchListings(filters);
|
|
|
|
return res.status(200).json(results);
|
|
}
|
|
|
|
async function handlePost(req: NextApiRequest, res: NextApiResponse) {
|
|
// In production, get sellerId from authenticated session
|
|
// For now, use a header or body field
|
|
const sellerId = req.headers['x-user-id'] as string || req.body.sellerId || 'anonymous';
|
|
const sellerName = req.headers['x-user-name'] as string || req.body.sellerName || 'Anonymous Seller';
|
|
|
|
const input: CreateListingInput = {
|
|
title: req.body.title,
|
|
description: req.body.description,
|
|
price: parseFloat(req.body.price),
|
|
currency: req.body.currency || 'USD',
|
|
quantity: parseInt(req.body.quantity, 10),
|
|
category: req.body.category,
|
|
plantId: req.body.plantId,
|
|
location: req.body.location,
|
|
tags: req.body.tags || [],
|
|
expiresAt: req.body.expiresAt ? new Date(req.body.expiresAt) : undefined,
|
|
};
|
|
|
|
// Validate required fields
|
|
if (!input.title || !input.description || !input.price || !input.quantity || !input.category) {
|
|
return res.status(400).json({
|
|
error: 'Missing required fields',
|
|
required: ['title', 'description', 'price', 'quantity', 'category'],
|
|
});
|
|
}
|
|
|
|
const listing = await listingService.createListing(sellerId, sellerName, input);
|
|
|
|
return res.status(201).json({
|
|
success: true,
|
|
listing,
|
|
});
|
|
}
|