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)
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
// API: Marketplace Recommendations
|
|
// GET /api/marketplace/recommendations - Get personalized recommendations
|
|
|
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
import { matchingService, searchService } from '@/lib/marketplace';
|
|
import type { ListingCategory } from '@/lib/marketplace/types';
|
|
import type { BuyerPreferences } from '@/lib/marketplace/matchingService';
|
|
|
|
export default async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse
|
|
) {
|
|
if (req.method !== 'GET') {
|
|
res.setHeader('Allow', ['GET']);
|
|
return res.status(405).json({ error: `Method ${req.method} Not Allowed` });
|
|
}
|
|
|
|
try {
|
|
const userId = req.headers['x-user-id'] as string || 'anonymous';
|
|
const { categories, maxPrice, lat, lng, radius, tags, limit: limitParam } = req.query;
|
|
|
|
const limit = limitParam ? parseInt(limitParam as string, 10) : 10;
|
|
|
|
// Build preferences from query params
|
|
const preferences: BuyerPreferences = {
|
|
categories: [],
|
|
};
|
|
|
|
if (categories) {
|
|
const categoryArray = Array.isArray(categories)
|
|
? categories
|
|
: (categories as string).split(',');
|
|
preferences.categories = categoryArray as ListingCategory[];
|
|
} else {
|
|
// Default to all categories if none specified
|
|
preferences.categories = Object.values(ListingCategory) as ListingCategory[];
|
|
}
|
|
|
|
if (maxPrice && typeof maxPrice === 'string') {
|
|
preferences.maxPrice = parseFloat(maxPrice);
|
|
}
|
|
|
|
if (lat && lng && radius) {
|
|
preferences.preferredLocation = {
|
|
lat: parseFloat(lat as string),
|
|
lng: parseFloat(lng as string),
|
|
maxDistanceKm: parseFloat(radius as string),
|
|
};
|
|
}
|
|
|
|
if (tags) {
|
|
const tagArray = Array.isArray(tags) ? tags : (tags as string).split(',');
|
|
preferences.preferredTags = tagArray.filter((t): t is string => typeof t === 'string');
|
|
}
|
|
|
|
// Get personalized matches
|
|
const matches = await matchingService.findMatchesForBuyer(userId, preferences, limit);
|
|
|
|
// Get general recommendations as fallback
|
|
const recommended = await matchingService.getRecommendedListings(userId, limit);
|
|
|
|
// Get similar items if we have matches
|
|
let similar = [];
|
|
if (matches.length > 0) {
|
|
similar = await searchService.getSimilarListings(matches[0].listing.id, 4);
|
|
}
|
|
|
|
return res.status(200).json({
|
|
matches: matches.map(m => ({
|
|
listing: m.listing,
|
|
score: m.score,
|
|
reasons: m.matchReasons,
|
|
})),
|
|
recommended,
|
|
similar,
|
|
preferences: {
|
|
categories: preferences.categories,
|
|
hasLocation: !!preferences.preferredLocation,
|
|
hasPriceLimit: !!preferences.maxPrice,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('Recommendations API error:', error);
|
|
return res.status(500).json({
|
|
error: 'Internal server error',
|
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
});
|
|
}
|
|
}
|