QA fixes: real listing creation, profile save, favorites, missing pages

- SellItemPage: real file upload + API listing creation + activate
- CreateProfilePage: save profile via PUT /users/profile
- ProductDetailPage: wire edit/delete/message buttons, show edit for owner
- ListingCard: persist favorites via API, show real images
- Footer: connect newsletter subscribe to API
- Router: add /dashboard/listings and /dashboard/saved routes
- Backend: add GET /listings/favorites endpoint
- New pages: MyListingsPage, SavedItemsPage
- Fix unused imports causing build failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
delta-lynx-89e8
2026-02-22 12:30:03 -08:00
parent 6722d1d4a1
commit d09c998d51
41 changed files with 3152 additions and 383 deletions

793
server/prisma/seed.ts Normal file
View File

@@ -0,0 +1,793 @@
import { PrismaClient } from '@prisma/client';
import bcryptjs from 'bcryptjs';
const { hashSync } = bcryptjs;
const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
const passwordHash = hashSync('password123', 10);
// ── Clear existing data (reverse dependency order) ──────────────────
await prisma.$transaction([
prisma.message.deleteMany(),
prisma.conversation.deleteMany(),
prisma.notification.deleteMany(),
prisma.payment.deleteMany(),
prisma.favorite.deleteMany(),
prisma.offer.deleteMany(),
prisma.listingImage.deleteMany(),
prisma.listing.deleteMany(),
prisma.blockedUser.deleteMany(),
prisma.session.deleteMany(),
prisma.user.deleteMany(),
]);
console.log('Cleared existing data.');
// ── Users ───────────────────────────────────────────────────────────
const users = await prisma.$transaction([
prisma.user.create({
data: {
id: 'user-alice',
email: 'alice.chen@example.com',
passwordHash,
fullName: 'Alice Chen',
nickname: 'alice',
avatar: '/uploads/avatars/alice.jpg',
phone: '(415) 555-0101',
location: 'San Francisco, CA',
bio: 'Tech enthusiast and avid reader. Always looking for great deals on gadgets and books.',
rating: 4.8,
ratingCount: 24,
showEmail: false,
showPhone: true,
showLocation: true,
showOnline: true,
showRating: true,
isActive: true,
marketingEmail: true,
},
}),
prisma.user.create({
data: {
id: 'user-bob',
email: 'bob.martinez@example.com',
passwordHash,
fullName: 'Bob Martinez',
nickname: 'bobby_m',
avatar: '/uploads/avatars/bob.jpg',
phone: '(213) 555-0202',
location: 'Los Angeles, CA',
bio: 'Sports memorabilia collector and weekend warrior. Selling gear I no longer use.',
rating: 4.5,
ratingCount: 18,
showEmail: true,
showPhone: true,
showLocation: true,
showOnline: true,
showRating: true,
isActive: true,
},
}),
prisma.user.create({
data: {
id: 'user-carol',
email: 'carol.nguyen@example.com',
passwordHash,
fullName: 'Carol Nguyen',
nickname: 'carol_n',
avatar: '/uploads/avatars/carol.jpg',
phone: '(503) 555-0303',
location: 'Portland, OR',
bio: 'Interior designer with an eye for vintage furniture. Downsizing my collection.',
rating: 4.9,
ratingCount: 32,
showEmail: false,
showPhone: false,
showLocation: true,
showOnline: false,
showRating: true,
isActive: true,
twoFactorEnabled: true,
},
}),
prisma.user.create({
data: {
id: 'user-david',
email: 'david.kim@example.com',
passwordHash,
fullName: 'David Kim',
nickname: 'dave_k',
avatar: '/uploads/avatars/david.jpg',
phone: '(312) 555-0404',
location: 'Chicago, IL',
bio: 'Gamer and music lover. Clearing out my shelves to make room for new hobbies.',
rating: 4.2,
ratingCount: 11,
showEmail: false,
showPhone: true,
showLocation: true,
showOnline: true,
showRating: true,
isActive: true,
},
}),
prisma.user.create({
data: {
id: 'user-eva',
email: 'eva.johnson@example.com',
passwordHash,
fullName: 'Eva Johnson',
nickname: 'eva_j',
avatar: '/uploads/avatars/eva.jpg',
phone: '(206) 555-0505',
location: 'Seattle, WA',
bio: 'Outdoor enthusiast and plant mom. Selling items that need a new home.',
rating: 4.7,
ratingCount: 15,
showEmail: true,
showPhone: true,
showLocation: true,
showOnline: true,
showRating: true,
isActive: true,
notifFavorites: false,
},
}),
]);
console.log(`Created ${users.length} users.`);
// ── Listings ────────────────────────────────────────────────────────
const listingsData = [
// ACTIVE listings (10)
{
id: 'listing-01',
title: 'MacBook Pro 14" M3 Pro — 18GB RAM',
description: 'Barely used MacBook Pro 14-inch with M3 Pro chip, 18GB unified memory, 512GB SSD. Includes original charger and box. AppleCare+ until March 2027. No scratches or dents, always used with a case.',
price: 1650.00,
obo: true,
category: 'ELECTRONICS' as const,
condition: 'LIKE_NEW' as const,
status: 'ACTIVE' as const,
location: 'San Francisco, CA',
viewCount: 142,
sellerId: 'user-alice',
},
{
id: 'listing-02',
title: 'Mid-Century Modern Walnut Coffee Table',
description: 'Beautiful solid walnut coffee table with tapered legs. Dimensions: 48"L x 24"W x 16"H. Minor surface wear consistent with age. A real statement piece for any living room.',
price: 320.00,
obo: true,
category: 'FURNITURE' as const,
condition: 'GENTLY_USED' as const,
status: 'ACTIVE' as const,
location: 'Portland, OR',
viewCount: 87,
sellerId: 'user-carol',
},
{
id: 'listing-03',
title: 'Patagonia Better Sweater Jacket — Men\'s Large',
description: 'Classic Patagonia fleece jacket in "New Navy" colorway, size Large. Worn a handful of times, no pilling or stains. Great for layering or casual wear.',
price: 75.00,
obo: false,
category: 'CLOTHING' as const,
condition: 'LIKE_NEW' as const,
status: 'ACTIVE' as const,
location: 'Seattle, WA',
viewCount: 63,
sellerId: 'user-eva',
},
{
id: 'listing-04',
title: 'Weber Spirit II E-310 Gas Grill',
description: 'Three-burner propane grill with side tables. Used for two summers. GS4 grilling system, porcelain-enameled cast-iron grates. Includes cover and propane tank. Pickup only.',
price: 280.00,
obo: true,
category: 'HOME_GARDEN' as const,
condition: 'USED' as const,
status: 'ACTIVE' as const,
location: 'Chicago, IL',
viewCount: 54,
sellerId: 'user-david',
},
{
id: 'listing-05',
title: 'Trek Marlin 7 Mountain Bike — Size M',
description: '2024 Trek Marlin 7 in Matte Nautical Navy. Size Medium (fits 5\'5"5\'9"). Shimano Deore 1x10 drivetrain, RockShox Judy fork. About 300 miles on it. Ready to ride.',
price: 620.00,
obo: true,
category: 'SPORTS' as const,
condition: 'GENTLY_USED' as const,
status: 'ACTIVE' as const,
location: 'Portland, OR',
viewCount: 109,
sellerId: 'user-carol',
},
{
id: 'listing-06',
title: 'Complete Dune Series (6 Books, Hardcover)',
description: 'The original six Dune novels by Frank Herbert in hardcover. Ace/Putnam editions with dust jackets. All in very good condition — no writing, highlighting, or torn pages.',
price: 95.00,
obo: false,
category: 'BOOKS' as const,
condition: 'GENTLY_USED' as const,
status: 'ACTIVE' as const,
location: 'San Francisco, CA',
viewCount: 76,
sellerId: 'user-alice',
},
{
id: 'listing-07',
title: 'PlayStation 5 Disc Edition + 4 Games',
description: 'PS5 Disc Edition console with two DualSense controllers, charging dock, and four games: Spider-Man 2, FF7 Rebirth, Elden Ring, and Baldur\'s Gate 3. Everything works perfectly.',
price: 420.00,
obo: true,
category: 'GAMES' as const,
condition: 'GENTLY_USED' as const,
status: 'ACTIVE' as const,
location: 'Chicago, IL',
viewCount: 203,
sellerId: 'user-david',
},
{
id: 'listing-08',
title: 'Sony WH-1000XM5 Wireless Headphones',
description: 'Industry-leading noise cancelling headphones in black. Includes carrying case, USB-C cable, and audio cable. Battery still holds 30+ hours. Ear pads in excellent shape.',
price: 199.00,
obo: false,
category: 'ELECTRONICS' as const,
condition: 'LIKE_NEW' as const,
status: 'ACTIVE' as const,
location: 'Los Angeles, CA',
viewCount: 91,
sellerId: 'user-bob',
},
{
id: 'listing-09',
title: 'Dyson V12 Detect Slim Cordless Vacuum',
description: 'Powerful cordless stick vacuum with laser dust detection. Comes with all original attachments and wall-mount dock. About 1 year old, works flawlessly.',
price: 340.00,
obo: true,
category: 'HOME_GARDEN' as const,
condition: 'GENTLY_USED' as const,
status: 'ACTIVE' as const,
location: 'Seattle, WA',
viewCount: 68,
sellerId: 'user-eva',
},
{
id: 'listing-10',
title: 'Vintage Schwinn Varsity Road Bike',
description: '1975 Schwinn Varsity 10-speed in original Kool Lemon color. All-original components, rides well. Some cosmetic patina adds character. A classic commuter or collector piece.',
price: 250.00,
obo: true,
category: 'VEHICLES' as const,
condition: 'FAIR' as const,
status: 'ACTIVE' as const,
location: 'Los Angeles, CA',
viewCount: 45,
sellerId: 'user-bob',
},
// SOLD listings (3)
{
id: 'listing-11',
title: 'Nintendo Switch OLED + Pro Controller',
description: 'White Nintendo Switch OLED model with Pro Controller, dock, and carrying case. Screen is pristine. Includes 3 physical game cartridges.',
price: 300.00,
obo: false,
category: 'GAMES' as const,
condition: 'GENTLY_USED' as const,
status: 'SOLD' as const,
location: 'San Francisco, CA',
viewCount: 178,
sellerId: 'user-alice',
},
{
id: 'listing-12',
title: 'Herman Miller Aeron Chair — Size B',
description: 'Fully loaded Aeron chair in graphite. Size B (medium). PostureFit SL, adjustable arms, tilt limiter. Purchased from authorized dealer in 2022. 12-year warranty still active.',
price: 750.00,
obo: true,
category: 'FURNITURE' as const,
condition: 'LIKE_NEW' as const,
status: 'SOLD' as const,
location: 'Portland, OR',
viewCount: 256,
sellerId: 'user-carol',
},
{
id: 'listing-13',
title: 'Canon EOS R6 Mark II Body Only',
description: 'Professional mirrorless camera body. 24.2MP full-frame sensor, 4K 60fps video, IBIS. Shutter count under 5,000. Includes battery, charger, and strap. No box.',
price: 1800.00,
obo: true,
category: 'ELECTRONICS' as const,
condition: 'LIKE_NEW' as const,
status: 'SOLD' as const,
location: 'Chicago, IL',
viewCount: 312,
sellerId: 'user-david',
},
// DRAFT listings (2)
{
id: 'listing-14',
title: 'Garmin Fenix 7X Solar Smartwatch',
description: 'Multi-sport GPS watch with solar charging. Titanium bezel, sapphire lens. Includes QuickFit bands. Still drafting — need to take photos.',
price: 450.00,
obo: true,
category: 'ELECTRONICS' as const,
condition: 'GENTLY_USED' as const,
status: 'DRAFT' as const,
location: 'Seattle, WA',
viewCount: 0,
sellerId: 'user-eva',
},
{
id: 'listing-15',
title: 'Restoration Hardware Cloud Sofa — 2 Seat',
description: 'RH Cloud sofa in white perennials fabric. Extremely comfortable. Moving and cannot bring it. Will update with measurements and photos shortly.',
price: 2200.00,
obo: true,
category: 'FURNITURE' as const,
condition: 'USED' as const,
status: 'DRAFT' as const,
location: 'Los Angeles, CA',
viewCount: 0,
sellerId: 'user-bob',
},
];
const listings = await prisma.$transaction(
listingsData.map((l) => prisma.listing.create({ data: l }))
);
console.log(`Created ${listings.length} listings.`);
// ── Listing Images ──────────────────────────────────────────────────
const imageRecords = [
{ listingId: 'listing-01', uploadedBy: 'user-alice', url: '/uploads/placeholder-1.jpg', order: 0 },
{ listingId: 'listing-01', uploadedBy: 'user-alice', url: '/uploads/placeholder-2.jpg', order: 1 },
{ listingId: 'listing-02', uploadedBy: 'user-carol', url: '/uploads/placeholder-3.jpg', order: 0 },
{ listingId: 'listing-02', uploadedBy: 'user-carol', url: '/uploads/placeholder-4.jpg', order: 1 },
{ listingId: 'listing-03', uploadedBy: 'user-eva', url: '/uploads/placeholder-5.jpg', order: 0 },
{ listingId: 'listing-04', uploadedBy: 'user-david', url: '/uploads/placeholder-6.jpg', order: 0 },
{ listingId: 'listing-04', uploadedBy: 'user-david', url: '/uploads/placeholder-7.jpg', order: 1 },
{ listingId: 'listing-05', uploadedBy: 'user-carol', url: '/uploads/placeholder-8.jpg', order: 0 },
{ listingId: 'listing-05', uploadedBy: 'user-carol', url: '/uploads/placeholder-9.jpg', order: 1 },
{ listingId: 'listing-06', uploadedBy: 'user-alice', url: '/uploads/placeholder-10.jpg', order: 0 },
{ listingId: 'listing-07', uploadedBy: 'user-david', url: '/uploads/placeholder-11.jpg', order: 0 },
{ listingId: 'listing-07', uploadedBy: 'user-david', url: '/uploads/placeholder-12.jpg', order: 1 },
{ listingId: 'listing-08', uploadedBy: 'user-bob', url: '/uploads/placeholder-13.jpg', order: 0 },
{ listingId: 'listing-09', uploadedBy: 'user-eva', url: '/uploads/placeholder-14.jpg', order: 0 },
{ listingId: 'listing-10', uploadedBy: 'user-bob', url: '/uploads/placeholder-15.jpg', order: 0 },
{ listingId: 'listing-10', uploadedBy: 'user-bob', url: '/uploads/placeholder-16.jpg', order: 1 },
{ listingId: 'listing-11', uploadedBy: 'user-alice', url: '/uploads/placeholder-17.jpg', order: 0 },
{ listingId: 'listing-12', uploadedBy: 'user-carol', url: '/uploads/placeholder-18.jpg', order: 0 },
{ listingId: 'listing-12', uploadedBy: 'user-carol', url: '/uploads/placeholder-19.jpg', order: 1 },
{ listingId: 'listing-13', uploadedBy: 'user-david', url: '/uploads/placeholder-20.jpg', order: 0 },
{ listingId: 'listing-13', uploadedBy: 'user-david', url: '/uploads/placeholder-21.jpg', order: 1 },
];
const images = await prisma.$transaction(
imageRecords.map((img) => prisma.listingImage.create({ data: img }))
);
console.log(`Created ${images.length} listing images.`);
// ── Offers ──────────────────────────────────────────────────────────
const now = new Date();
const inOneWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const daysAgo = (d: number) => new Date(now.getTime() - d * 24 * 60 * 60 * 1000);
const offersData = [
// ACCEPTED offers on SOLD listings
{
id: 'offer-01',
amount: 275.00,
message: 'Would you take $275? I can pick it up today.',
status: 'ACCEPTED' as const,
buyerId: 'user-bob',
sellerId: 'user-alice',
listingId: 'listing-11',
expiresAt: daysAgo(-2),
createdAt: daysAgo(10),
},
{
id: 'offer-02',
amount: 680.00,
message: 'Beautiful chair! I have been looking for one of these. Would $680 work?',
status: 'ACCEPTED' as const,
counterAmount: 720.00,
buyerId: 'user-eva',
sellerId: 'user-carol',
listingId: 'listing-12',
expiresAt: daysAgo(-1),
createdAt: daysAgo(14),
},
{
id: 'offer-03',
amount: 1700.00,
message: 'Interested in the R6 II. Offering $1700 — can meet anywhere in Chicago.',
status: 'ACCEPTED' as const,
buyerId: 'user-alice',
sellerId: 'user-david',
listingId: 'listing-13',
expiresAt: daysAgo(-3),
createdAt: daysAgo(7),
},
// PENDING offers
{
id: 'offer-04',
amount: 1500.00,
message: 'Is there any flexibility on price? I see similar configs going for around $1500.',
status: 'PENDING' as const,
buyerId: 'user-david',
sellerId: 'user-alice',
listingId: 'listing-01',
expiresAt: inOneWeek,
createdAt: daysAgo(1),
},
{
id: 'offer-05',
amount: 380.00,
message: 'Great deal on the PS5 bundle. Would you accept $380?',
status: 'PENDING' as const,
buyerId: 'user-carol',
sellerId: 'user-david',
listingId: 'listing-07',
expiresAt: inOneWeek,
createdAt: daysAgo(2),
},
{
id: 'offer-06',
amount: 550.00,
message: 'Love this bike! Would you consider $550?',
status: 'PENDING' as const,
buyerId: 'user-alice',
sellerId: 'user-carol',
listingId: 'listing-05',
expiresAt: inOneWeek,
createdAt: daysAgo(1),
},
// DECLINED offers
{
id: 'offer-07',
amount: 200.00,
message: 'I can offer $200 for the coffee table.',
status: 'DECLINED' as const,
buyerId: 'user-david',
sellerId: 'user-carol',
listingId: 'listing-02',
expiresAt: daysAgo(1),
createdAt: daysAgo(5),
},
{
id: 'offer-08',
amount: 150.00,
message: 'Budget is tight but really want those headphones. $150?',
status: 'DECLINED' as const,
buyerId: 'user-eva',
sellerId: 'user-bob',
listingId: 'listing-08',
expiresAt: daysAgo(2),
createdAt: daysAgo(8),
},
// COUNTERED offers
{
id: 'offer-09',
amount: 250.00,
message: 'Interested in the grill. How about $250?',
status: 'COUNTERED' as const,
counterAmount: 270.00,
buyerId: 'user-alice',
sellerId: 'user-david',
listingId: 'listing-04',
expiresAt: inOneWeek,
createdAt: daysAgo(3),
},
{
id: 'offer-10',
amount: 290.00,
message: 'Would $290 work for the vacuum? I am in the Ballard area and can pick up.',
status: 'COUNTERED' as const,
counterAmount: 310.00,
buyerId: 'user-bob',
sellerId: 'user-eva',
listingId: 'listing-09',
expiresAt: inOneWeek,
createdAt: daysAgo(2),
},
];
const offers = await prisma.$transaction(
offersData.map((o) => prisma.offer.create({ data: o }))
);
console.log(`Created ${offers.length} offers.`);
// ── Conversations & Messages ────────────────────────────────────────
const conv1 = await prisma.conversation.create({
data: {
id: 'conv-01',
user1Id: 'user-bob',
user2Id: 'user-alice',
listingId: 'listing-11',
},
});
const conv2 = await prisma.conversation.create({
data: {
id: 'conv-02',
user1Id: 'user-eva',
user2Id: 'user-carol',
listingId: 'listing-12',
},
});
const conv3 = await prisma.conversation.create({
data: {
id: 'conv-03',
user1Id: 'user-david',
user2Id: 'user-alice',
listingId: 'listing-01',
},
});
const conv4 = await prisma.conversation.create({
data: {
id: 'conv-04',
user1Id: 'user-alice',
user2Id: 'user-david',
listingId: 'listing-04',
},
});
console.log('Created 4 conversations.');
const messagesData = [
// Conv 1: Bob buying Switch from Alice (completed sale)
{ content: 'Hi! Is the Switch still available?', senderId: 'user-bob', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(11) },
{ content: 'Yes it is! Everything works great.', senderId: 'user-alice', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(11) },
{ content: 'Awesome. Would you take $275 for it?', senderId: 'user-bob', conversationId: 'conv-01', isRead: true, offerAmount: 275.00, createdAt: daysAgo(10) },
{ content: 'That works for me. When can you pick up?', senderId: 'user-alice', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(10) },
{ content: 'How about Saturday around noon? I can come to the Mission district.', senderId: 'user-bob', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(9) },
{ content: 'Perfect. I will DM you the exact address on Saturday morning.', senderId: 'user-alice', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(9) },
{ content: 'Picked it up — everything looks great. Thanks Alice!', senderId: 'user-bob', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(7) },
// Conv 2: Eva buying Aeron from Carol (completed sale)
{ content: 'Hi Carol, love the Aeron listing! Is the warranty transferable?', senderId: 'user-eva', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(15) },
{ content: 'Hi Eva! Yes, Herman Miller warranties follow the chair, not the owner.', senderId: 'user-carol', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(15) },
{ content: 'Great to know. Would $680 be fair? I see refurbs going for about that.', senderId: 'user-eva', conversationId: 'conv-02', isRead: true, offerAmount: 680.00, createdAt: daysAgo(14) },
{ content: 'This is from an authorized dealer though. How about we meet at $720?', senderId: 'user-carol', conversationId: 'conv-02', isRead: true, offerAmount: 720.00, createdAt: daysAgo(14) },
{ content: '$720 is fair. Deal! Can you ship to Seattle or is it pickup only?', senderId: 'user-eva', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(13) },
{ content: 'I can ship via UPS freight. I will factor that into the price — still $720 total.', senderId: 'user-carol', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(13) },
{ content: 'That is incredibly generous. Sending payment now.', senderId: 'user-eva', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(12) },
{ content: 'Payment received! Shipping Monday. Will send tracking.', senderId: 'user-carol', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(12) },
// Conv 3: David interested in MacBook from Alice (ongoing)
{ content: 'Hey, quick question about the MacBook — what is the battery cycle count?', senderId: 'user-david', conversationId: 'conv-03', isRead: true, createdAt: daysAgo(3) },
{ content: 'Let me check... 47 cycles. Basically brand new battery.', senderId: 'user-alice', conversationId: 'conv-03', isRead: true, createdAt: daysAgo(3) },
{ content: 'Nice. Any issues with the keyboard or trackpad?', senderId: 'user-david', conversationId: 'conv-03', isRead: true, createdAt: daysAgo(2) },
{ content: 'Nope, everything is perfect. I mostly used it with an external display and keyboard.', senderId: 'user-alice', conversationId: 'conv-03', isRead: true, createdAt: daysAgo(2) },
{ content: 'I submitted an offer for $1500 — let me know what you think.', senderId: 'user-david', conversationId: 'conv-03', isRead: true, offerAmount: 1500.00, createdAt: daysAgo(1) },
{ content: 'I will take a look. That is a bit lower than I was hoping for but let me think about it.', senderId: 'user-alice', conversationId: 'conv-03', isRead: false, createdAt: daysAgo(0) },
// Conv 4: Alice interested in grill from David (ongoing)
{ content: 'Hi David! Does the grill come with the cover shown in the photo?', senderId: 'user-alice', conversationId: 'conv-04', isRead: true, createdAt: daysAgo(4) },
{ content: 'Yes, the cover and propane tank are both included.', senderId: 'user-david', conversationId: 'conv-04', isRead: true, createdAt: daysAgo(4) },
{ content: 'I put in an offer for $250. Let me know if that works.', senderId: 'user-alice', conversationId: 'conv-04', isRead: true, offerAmount: 250.00, createdAt: daysAgo(3) },
{ content: 'Appreciate the offer! Could you do $270? I paid $500 new last year.', senderId: 'user-david', conversationId: 'conv-04', isRead: true, offerAmount: 270.00, createdAt: daysAgo(3) },
{ content: 'Let me think on it and get back to you.', senderId: 'user-alice', conversationId: 'conv-04', isRead: false, createdAt: daysAgo(2) },
];
const messages = await prisma.$transaction(
messagesData.map((m) => prisma.message.create({ data: m }))
);
console.log(`Created ${messages.length} messages.`);
// ── Favorites ───────────────────────────────────────────────────────
const favoritesData = [
{ userId: 'user-bob', listingId: 'listing-01' },
{ userId: 'user-bob', listingId: 'listing-06' },
{ userId: 'user-carol', listingId: 'listing-07' },
{ userId: 'user-carol', listingId: 'listing-08' },
{ userId: 'user-david', listingId: 'listing-02' },
{ userId: 'user-david', listingId: 'listing-05' },
{ userId: 'user-eva', listingId: 'listing-01' },
{ userId: 'user-eva', listingId: 'listing-07' },
{ userId: 'user-alice', listingId: 'listing-04' },
{ userId: 'user-alice', listingId: 'listing-05' },
{ userId: 'user-alice', listingId: 'listing-08' },
];
const favorites = await prisma.$transaction(
favoritesData.map((f) => prisma.favorite.create({ data: f }))
);
console.log(`Created ${favorites.length} favorites.`);
// ── Notifications ───────────────────────────────────────────────────
const notificationsData = [
// NEW_OFFER
{
userId: 'user-alice',
type: 'NEW_OFFER' as const,
title: 'New Offer Received',
body: 'David Kim offered $1,500 for your MacBook Pro 14" M3 Pro.',
data: { offerId: 'offer-04', listingId: 'listing-01' },
isRead: false,
createdAt: daysAgo(1),
},
{
userId: 'user-david',
type: 'NEW_OFFER' as const,
title: 'New Offer Received',
body: 'Carol Nguyen offered $380 for your PlayStation 5 Disc Edition.',
data: { offerId: 'offer-05', listingId: 'listing-07' },
isRead: true,
createdAt: daysAgo(2),
},
// OFFER_ACCEPTED
{
userId: 'user-bob',
type: 'OFFER_ACCEPTED' as const,
title: 'Offer Accepted!',
body: 'Alice Chen accepted your offer of $275 for the Nintendo Switch OLED.',
data: { offerId: 'offer-01', listingId: 'listing-11' },
isRead: true,
createdAt: daysAgo(10),
},
{
userId: 'user-alice',
type: 'OFFER_ACCEPTED' as const,
title: 'Offer Accepted!',
body: 'David Kim accepted your offer of $1,700 for the Canon EOS R6 Mark II.',
data: { offerId: 'offer-03', listingId: 'listing-13' },
isRead: true,
createdAt: daysAgo(7),
},
// OFFER_DECLINED
{
userId: 'user-david',
type: 'OFFER_DECLINED' as const,
title: 'Offer Declined',
body: 'Carol Nguyen declined your offer of $200 for the Mid-Century Coffee Table.',
data: { offerId: 'offer-07', listingId: 'listing-02' },
isRead: true,
createdAt: daysAgo(5),
},
{
userId: 'user-eva',
type: 'OFFER_DECLINED' as const,
title: 'Offer Declined',
body: 'Bob Martinez declined your offer of $150 for the Sony WH-1000XM5.',
data: { offerId: 'offer-08', listingId: 'listing-08' },
isRead: false,
createdAt: daysAgo(8),
},
// ITEM_SOLD
{
userId: 'user-alice',
type: 'ITEM_SOLD' as const,
title: 'Item Sold!',
body: 'Your Nintendo Switch OLED has been sold for $275.',
data: { listingId: 'listing-11', amount: 275 },
isRead: true,
createdAt: daysAgo(9),
},
{
userId: 'user-carol',
type: 'ITEM_SOLD' as const,
title: 'Item Sold!',
body: 'Your Herman Miller Aeron Chair has been sold for $720.',
data: { listingId: 'listing-12', amount: 720 },
isRead: true,
createdAt: daysAgo(12),
},
// NEW_MESSAGE
{
userId: 'user-alice',
type: 'NEW_MESSAGE' as const,
title: 'New Message',
body: 'David Kim sent you a message about MacBook Pro 14" M3 Pro.',
data: { conversationId: 'conv-03', listingId: 'listing-01' },
isRead: true,
createdAt: daysAgo(3),
},
{
userId: 'user-david',
type: 'NEW_MESSAGE' as const,
title: 'New Message',
body: 'Alice Chen sent you a message about Weber Spirit II Gas Grill.',
data: { conversationId: 'conv-04', listingId: 'listing-04' },
isRead: false,
createdAt: daysAgo(2),
},
// ITEM_FAVORITED
{
userId: 'user-alice',
type: 'ITEM_FAVORITED' as const,
title: 'Item Favorited',
body: 'Someone favorited your MacBook Pro 14" M3 Pro listing.',
data: { listingId: 'listing-01' },
isRead: false,
createdAt: daysAgo(1),
},
{
userId: 'user-carol',
type: 'ITEM_FAVORITED' as const,
title: 'Item Favorited',
body: 'Someone favorited your Trek Marlin 7 Mountain Bike listing.',
data: { listingId: 'listing-05' },
isRead: true,
createdAt: daysAgo(4),
},
];
const notifications = await prisma.$transaction(
notificationsData.map((n) => prisma.notification.create({ data: n }))
);
console.log(`Created ${notifications.length} notifications.`);
// ── Payments (3 completed for sold items) ───────────────────────────
const paymentsData = [
{
userId: 'user-bob',
listingId: 'listing-11',
stripePaymentId: 'pi_3PxSwitch001',
amount: 275.00,
status: 'COMPLETED' as const,
createdAt: daysAgo(9),
},
{
userId: 'user-eva',
listingId: 'listing-12',
stripePaymentId: 'pi_3PxAeron002',
amount: 720.00,
status: 'COMPLETED' as const,
createdAt: daysAgo(12),
},
{
userId: 'user-alice',
listingId: 'listing-13',
stripePaymentId: 'pi_3PxCanon003',
amount: 1700.00,
status: 'COMPLETED' as const,
createdAt: daysAgo(6),
},
];
const payments = await prisma.$transaction(
paymentsData.map((p) => prisma.payment.create({ data: p }))
);
console.log(`Created ${payments.length} payments.`);
// ── Blocked Users ───────────────────────────────────────────────────
await prisma.blockedUser.create({
data: {
blockerId: 'user-carol',
blockedId: 'user-david',
},
});
console.log('Created 1 blocked user entry.');
console.log('\nSeed completed successfully!');
}
main()
.catch((e) => {
console.error('Seed failed:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});