Schema:
- Add NotificationType enum (FOLLOW, REACTION, COMMENT, MESSAGE)
- Add Notification model with recipientPet, actorPet, post, comment
relations; index on [recipientPetId, read, createdAt desc]
Backend:
- notifications router: list (cursor-paginated), unreadCount,
markRead, markAllRead
- follows.create: fire-and-forget FOLLOW notification to followee
- reactions.toggle: REACTION notification on create (skip self)
- comments.create: COMMENT notification with commentId (skip self)
All triggers use .catch(()=>{}) to never fail the main action
UI:
- NotificationBell component with orange badge, polls every 30s
- /notifications page: list with avatar, type icon, text, thumbnail,
read/unread dot; auto-marks all read on visit
- Sidebar: Notifications link activated with NotificationBell
- MobileNav: Bell replaces Search tab (Search still at /search)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
299 lines
9.4 KiB
Plaintext
299 lines
9.4 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
model Owner {
|
|
id String @id // = Clerk userId (e.g. "user_xxx")
|
|
createdAt DateTime @default(now())
|
|
pets Pet[]
|
|
}
|
|
|
|
model Species {
|
|
id String @id @default(cuid())
|
|
name String @unique // "Dog" | "Cat" | "Bird"
|
|
breeds Breed[]
|
|
pets Pet[]
|
|
}
|
|
|
|
model Breed {
|
|
id String @id @default(cuid())
|
|
name String
|
|
species Species @relation(fields: [speciesId], references: [id])
|
|
speciesId String
|
|
pets Pet[]
|
|
|
|
@@unique([speciesId, name])
|
|
}
|
|
|
|
model Pet {
|
|
id String @id @default(cuid())
|
|
owner Owner @relation(fields: [ownerId], references: [id])
|
|
ownerId String
|
|
species Species @relation(fields: [speciesId], references: [id])
|
|
speciesId String
|
|
breed Breed? @relation(fields: [breedId], references: [id])
|
|
breedId String?
|
|
name String
|
|
bio String?
|
|
adoptionStory String?
|
|
avatarKey String? // Supabase Storage object key, NOT a full URL
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Phase 2 back-relations
|
|
posts Post[]
|
|
stories Story[]
|
|
storyViews StoryView[] @relation("StoryViewer")
|
|
following Follow[] @relation("Follower")
|
|
followers Follow[] @relation("Followee")
|
|
blocking Block[] @relation("Blocker")
|
|
blockedBy Block[] @relation("Blocked")
|
|
reports Report[] @relation("Reporter")
|
|
|
|
// Phase 3 back-relations
|
|
reactions Reaction[] @relation("PetReactions")
|
|
comments Comment[] @relation("PetComments")
|
|
reposts Repost[] @relation("PetReposts")
|
|
|
|
// Phase 4 back-relations
|
|
receivedNotifications Notification[] @relation("ReceivedNotifications")
|
|
sentNotifications Notification[] @relation("SentNotifications")
|
|
|
|
@@index([ownerId])
|
|
}
|
|
|
|
// ─── Phase 2 Enums ───────────────────────────────────────────────────────────
|
|
|
|
enum PostType {
|
|
PHOTO
|
|
MILESTONE
|
|
REPOST
|
|
}
|
|
|
|
enum MilestoneType {
|
|
BIRTHDAY
|
|
ADOPTION_DAY
|
|
FIRST_OUTING
|
|
VET_VISIT
|
|
LITTER
|
|
FOLLOWERS_500
|
|
FOLLOWERS_1K
|
|
FOLLOWERS_2_5K
|
|
FOLLOWERS_5K
|
|
FOLLOWERS_10K
|
|
FOLLOWERS_25K
|
|
FOLLOWERS_50K
|
|
FOLLOWERS_100K
|
|
FOLLOWERS_250K
|
|
FOLLOWERS_500K
|
|
FOLLOWERS_1M
|
|
}
|
|
|
|
enum ReportReason {
|
|
SPAM
|
|
HARASSMENT
|
|
INAPPROPRIATE
|
|
MISINFORMATION
|
|
OTHER
|
|
}
|
|
|
|
// ─── Phase 2 Models ──────────────────────────────────────────────────────────
|
|
|
|
model Post {
|
|
id String @id @default(cuid())
|
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
type PostType @default(PHOTO)
|
|
caption String? @db.VarChar(500)
|
|
images PostImage[]
|
|
milestone Milestone?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Phase 3 back-relations
|
|
reactions Reaction[] @relation("PostReactions")
|
|
comments Comment[] @relation("PostComments")
|
|
repost Repost? @relation("RepostEntry")
|
|
originalReposts Repost[] @relation("OriginalReposts")
|
|
hashtags PostHashtag[] @relation("PostHashtags")
|
|
|
|
// Phase 4 back-relations
|
|
notifications Notification[]
|
|
|
|
@@index([petId, createdAt(sort: Desc)])
|
|
}
|
|
|
|
model PostImage {
|
|
id String @id @default(cuid())
|
|
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String
|
|
storageKey String // Supabase Storage object key
|
|
position Int // 0-based order in carousel
|
|
width Int?
|
|
height Int?
|
|
|
|
@@unique([postId, position])
|
|
@@index([postId])
|
|
}
|
|
|
|
model Milestone {
|
|
id String @id @default(cuid())
|
|
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String @unique
|
|
milestoneType MilestoneType
|
|
notes String? @db.VarChar(500) // free text for 'litter' type (D-09)
|
|
}
|
|
|
|
model Story {
|
|
id String @id @default(cuid())
|
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
storageKey String // Supabase Storage object key
|
|
expiresAt DateTime // set to NOW() + 24 hours on create (D-07)
|
|
views StoryView[]
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([petId, expiresAt]) // supports lazy-delete filter query
|
|
}
|
|
|
|
model StoryView {
|
|
story Story @relation(fields: [storyId], references: [id]) // NO onDelete:Cascade (D-08)
|
|
storyId String
|
|
viewerPet Pet @relation("StoryViewer", fields: [viewerPetId], references: [id])
|
|
viewerPetId String
|
|
viewedAt DateTime @default(now())
|
|
|
|
@@id([storyId, viewerPetId]) // deduplication: one view per viewer per story
|
|
@@index([viewedAt]) // supports 30-day cleanup job (D-08)
|
|
}
|
|
|
|
model Follow {
|
|
follower Pet @relation("Follower", fields: [followerPetId], references: [id], onDelete: Cascade)
|
|
followerPetId String
|
|
followee Pet @relation("Followee", fields: [followeePetId], references: [id], onDelete: Cascade)
|
|
followeePetId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([followerPetId, followeePetId])
|
|
@@index([followerPetId])
|
|
@@index([followeePetId])
|
|
}
|
|
|
|
model Block {
|
|
blocker Pet @relation("Blocker", fields: [blockerPetId], references: [id], onDelete: Cascade)
|
|
blockerPetId String
|
|
blocked Pet @relation("Blocked", fields: [blockedPetId], references: [id], onDelete: Cascade)
|
|
blockedPetId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([blockerPetId, blockedPetId])
|
|
@@index([blockerPetId])
|
|
}
|
|
|
|
model Report {
|
|
id String @id @default(cuid())
|
|
reporter Pet @relation("Reporter", fields: [reporterPetId], references: [id])
|
|
reporterPetId String
|
|
targetPostId String? // null if reporting a profile
|
|
targetPetId String? // null if reporting a post
|
|
reason ReportReason
|
|
notes String? @db.VarChar(500)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([reporterPetId])
|
|
}
|
|
|
|
// ─── Phase 3 Models ──────────────────────────────────────────────────────────
|
|
|
|
model Reaction {
|
|
id String @id @default(cuid())
|
|
post Post @relation("PostReactions", fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String
|
|
pet Pet @relation("PetReactions", fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([postId, petId])
|
|
@@index([postId])
|
|
}
|
|
|
|
model Comment {
|
|
id String @id @default(cuid())
|
|
post Post @relation("PostComments", fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String
|
|
pet Pet @relation("PetComments", fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
body String @db.VarChar(500)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Phase 4 back-relation
|
|
notifications Notification[]
|
|
|
|
@@index([postId, createdAt(sort: Asc)])
|
|
@@index([petId])
|
|
}
|
|
|
|
model Repost {
|
|
post Post @relation("RepostEntry", fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String @unique
|
|
originalPost Post @relation("OriginalReposts", fields: [originalPostId], references: [id])
|
|
originalPostId String
|
|
reposterPet Pet @relation("PetReposts", fields: [reposterPetId], references: [id], onDelete: Cascade)
|
|
reposterPetId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([reposterPetId, originalPostId])
|
|
@@index([originalPostId])
|
|
}
|
|
|
|
model Hashtag {
|
|
id String @id @default(cuid())
|
|
tag String @unique
|
|
posts PostHashtag[]
|
|
|
|
@@index([tag])
|
|
}
|
|
|
|
model PostHashtag {
|
|
post Post @relation("PostHashtags", fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String
|
|
hashtag Hashtag @relation(fields: [hashtagId], references: [id], onDelete: Cascade)
|
|
hashtagId String
|
|
|
|
@@id([postId, hashtagId])
|
|
@@index([hashtagId])
|
|
}
|
|
|
|
// ─── Phase 4 Enums ───────────────────────────────────────────────────────────
|
|
|
|
enum NotificationType {
|
|
FOLLOW
|
|
REACTION
|
|
COMMENT
|
|
MESSAGE
|
|
}
|
|
|
|
// ─── Phase 4 Models ──────────────────────────────────────────────────────────
|
|
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
recipientPet Pet @relation("ReceivedNotifications", fields: [recipientPetId], references: [id], onDelete: Cascade)
|
|
recipientPetId String
|
|
type NotificationType
|
|
actorPet Pet? @relation("SentNotifications", fields: [actorPetId], references: [id], onDelete: SetNull)
|
|
actorPetId String?
|
|
post Post? @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String?
|
|
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
|
commentId String?
|
|
read Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([recipientPetId, read, createdAt(sort: Desc)])
|
|
}
|