Full-stack marketplace for buying/selling second-hand items. React 19 + TypeScript + Tailwind CSS v4 frontend with 17 screens, Express + Prisma + Socket.io backend, Stripe payments, JWT auth. Deployed at https://marketplace.173.212.212.157.sslip.io/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
263 lines
6.6 KiB
Plaintext
263 lines
6.6 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
enum Category {
|
|
ELECTRONICS
|
|
FURNITURE
|
|
CLOTHING
|
|
HOME_GARDEN
|
|
SPORTS
|
|
BOOKS
|
|
GAMES
|
|
VEHICLES
|
|
OTHER
|
|
}
|
|
|
|
enum ListingCondition {
|
|
NEW
|
|
LIKE_NEW
|
|
GENTLY_USED
|
|
USED
|
|
FAIR
|
|
}
|
|
|
|
enum ListingStatus {
|
|
DRAFT
|
|
ACTIVE
|
|
SOLD
|
|
DELETED
|
|
}
|
|
|
|
enum OfferStatus {
|
|
PENDING
|
|
ACCEPTED
|
|
DECLINED
|
|
COUNTERED
|
|
}
|
|
|
|
enum NotificationType {
|
|
NEW_OFFER
|
|
OFFER_ACCEPTED
|
|
OFFER_DECLINED
|
|
ITEM_SOLD
|
|
NEW_MESSAGE
|
|
ITEM_FAVORITED
|
|
}
|
|
|
|
enum PaymentStatus {
|
|
PENDING
|
|
COMPLETED
|
|
FAILED
|
|
REFUNDED
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
passwordHash String
|
|
fullName String
|
|
nickname String?
|
|
avatar String?
|
|
phone String?
|
|
location String?
|
|
bio String?
|
|
rating Float @default(0)
|
|
ratingCount Int @default(0)
|
|
showEmail Boolean @default(false)
|
|
showPhone Boolean @default(true)
|
|
showLocation Boolean @default(true)
|
|
showOnline Boolean @default(true)
|
|
showRating Boolean @default(true)
|
|
twoFactorEnabled Boolean @default(false)
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
sessions Session[]
|
|
listings Listing[]
|
|
images ListingImage[]
|
|
sentOffers Offer[] @relation("BuyerOffers")
|
|
receivedOffers Offer[] @relation("SellerOffers")
|
|
conversations1 Conversation[] @relation("User1Conversations")
|
|
conversations2 Conversation[] @relation("User2Conversations")
|
|
messages Message[]
|
|
favorites Favorite[]
|
|
notifications Notification[]
|
|
payments Payment[]
|
|
blockedUsers BlockedUser[] @relation("Blocker")
|
|
blockedBy BlockedUser[] @relation("Blocked")
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
refreshToken String @unique
|
|
userAgent String?
|
|
ipAddress String?
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
model Listing {
|
|
id String @id @default(cuid())
|
|
title String
|
|
description String
|
|
price Float
|
|
obo Boolean @default(false)
|
|
category Category
|
|
condition ListingCondition
|
|
status ListingStatus @default(DRAFT)
|
|
location String
|
|
viewCount Int @default(0)
|
|
sellerId String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
seller User @relation(fields: [sellerId], references: [id], onDelete: Cascade)
|
|
images ListingImage[]
|
|
offers Offer[]
|
|
conversations Conversation[]
|
|
favorites Favorite[]
|
|
payments Payment[]
|
|
|
|
@@index([sellerId])
|
|
@@index([category])
|
|
@@index([status])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model ListingImage {
|
|
id String @id @default(cuid())
|
|
url String
|
|
order Int @default(0)
|
|
listingId String
|
|
uploadedBy String
|
|
|
|
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [uploadedBy], references: [id], onDelete: Cascade)
|
|
|
|
@@index([listingId])
|
|
}
|
|
|
|
model Offer {
|
|
id String @id @default(cuid())
|
|
amount Float
|
|
message String?
|
|
status OfferStatus @default(PENDING)
|
|
counterAmount Float?
|
|
buyerId String
|
|
sellerId String
|
|
listingId String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
buyer User @relation("BuyerOffers", fields: [buyerId], references: [id], onDelete: Cascade)
|
|
seller User @relation("SellerOffers", fields: [sellerId], references: [id], onDelete: Cascade)
|
|
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([buyerId])
|
|
@@index([sellerId])
|
|
@@index([listingId])
|
|
}
|
|
|
|
model Conversation {
|
|
id String @id @default(cuid())
|
|
user1Id String
|
|
user2Id String
|
|
listingId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user1 User @relation("User1Conversations", fields: [user1Id], references: [id], onDelete: Cascade)
|
|
user2 User @relation("User2Conversations", fields: [user2Id], references: [id], onDelete: Cascade)
|
|
listing Listing? @relation(fields: [listingId], references: [id], onDelete: SetNull)
|
|
messages Message[]
|
|
|
|
@@unique([user1Id, user2Id, listingId])
|
|
@@index([user1Id])
|
|
@@index([user2Id])
|
|
}
|
|
|
|
model Message {
|
|
id String @id @default(cuid())
|
|
content String
|
|
senderId String
|
|
conversationId String
|
|
isRead Boolean @default(false)
|
|
offerAmount Float?
|
|
createdAt DateTime @default(now())
|
|
|
|
sender User @relation(fields: [senderId], references: [id], onDelete: Cascade)
|
|
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([conversationId])
|
|
@@index([senderId])
|
|
}
|
|
|
|
model Favorite {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
listingId String
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, listingId])
|
|
}
|
|
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type NotificationType
|
|
title String
|
|
body String
|
|
data Json?
|
|
isRead Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model Payment {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
listingId String
|
|
stripePaymentId String? @unique
|
|
amount Float
|
|
status PaymentStatus @default(PENDING)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@index([listingId])
|
|
}
|
|
|
|
model BlockedUser {
|
|
id String @id @default(cuid())
|
|
blockerId String
|
|
blockedId String
|
|
createdAt DateTime @default(now())
|
|
|
|
blocker User @relation("Blocker", fields: [blockerId], references: [id], onDelete: Cascade)
|
|
blocked User @relation("Blocked", fields: [blockedId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([blockerId, blockedId])
|
|
}
|