diff --git a/package.json b/package.json index 70089b1..0096267 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,8 @@ "tailwindcss": "^4.0.6", "tw-animate-css": "^1.3.6", "vite-tsconfig-paths": "^6.0.5", - "zod": "^4.3.6" + "zod": "^4.3.6", + "zustand": "^5.0.11" }, "devDependencies": { "@inlang/paraglide-js": "2.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b66164..c7b2a32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: zod: specifier: ^4.3.6 version: 4.3.6 + zustand: + specifier: ^5.0.11 + version: 5.0.11(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: '@inlang/paraglide-js': specifier: 2.10.0 @@ -4866,6 +4869,24 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zustand@5.0.11: + resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@acemir/cssom@0.9.31': {} @@ -9690,3 +9711,9 @@ snapshots: zod@3.25.76: {} zod@4.3.6: {} + + zustand@5.0.11(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + optionalDependencies: + '@types/react': 19.2.10 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) diff --git a/prisma/data.ts b/prisma/data.ts index adef299..9e00b35 100644 --- a/prisma/data.ts +++ b/prisma/data.ts @@ -15,3 +15,14 @@ export const settingsData = [ description: 'The keywords of the site', }, ]; + +export const userData = [ + { + name: 'Raysam', + email: 'raysam024@gmail.com', + }, + { + name: 'Raysam', + email: 'juines.liu@gmail.com', + }, +]; diff --git a/prisma/migrations/20260214113407_notification/migration.sql b/prisma/migrations/20260221093213_notification/migration.sql similarity index 91% rename from prisma/migrations/20260214113407_notification/migration.sql rename to prisma/migrations/20260221093213_notification/migration.sql index f284c89..3a269e3 100644 --- a/prisma/migrations/20260214113407_notification/migration.sql +++ b/prisma/migrations/20260221093213_notification/migration.sql @@ -48,7 +48,10 @@ CREATE TABLE "notification" ( ); -- CreateIndex -CREATE INDEX "notification_userId_idx" ON "notification"("userId"); +CREATE INDEX "notification_userId_readAt_idx" ON "notification"("userId", "readAt"); + +-- CreateIndex +CREATE INDEX "notification_readAt_idx" ON "notification"("readAt"); -- AddForeignKey ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 10ec0c2..a88dabb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -176,6 +176,7 @@ model Notification { user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@index([userId]) + @@index([userId, readAt]) + @@index([readAt]) @@map("notification") } diff --git a/prisma/seed.ts b/prisma/seed.ts index bf6e5b6..fe80209 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,7 +1,7 @@ import { auth } from '@lib/auth'; import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '../src/generated/prisma/client.js'; -import { settingsData } from './data.js'; +import { settingsData, userData } from './data.js'; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL!, @@ -32,6 +32,19 @@ async function main() { } console.log('---------------Created admin user-----------------'); + + userData.map(async (user) => { + await auth.api.createUser({ + body: { + email: user.email, + password: 'Th@m!S@m!040390', + name: user.name, + role: 'user', + }, + }); + }); + + console.log('---------------Created member user-----------------'); await prisma.setting.deleteMany(); const listSettings = [ diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index dbee7cd..e6351e7 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -1,9 +1,11 @@ +import { updateReadedNotification } from '@/service/notify.api'; import { notificationQueries } from '@/service/queries'; +import useNotificationStore from '@/store/useNotificationStore'; import { formatTimeAgo } from '@/utils/helper'; import { cn } from '@lib/utils'; import { m } from '@paraglide/messages'; import { BellIcon } from '@phosphor-icons/react'; -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { Link } from '@tanstack/react-router'; import { Button } from '@ui/button'; import { @@ -15,15 +17,42 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@ui/dropdown-menu'; +import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; import { Item, ItemContent, ItemDescription, ItemTitle } from './ui/item'; const Notification = () => { - const { data: notifications } = useQuery(notificationQueries.topFive()); + const [open, _setOpen] = useState(false); + const { hasNew, setHasNew } = useNotificationStore((state) => state); + const { data } = useQuery(notificationQueries.topFive()); - if (!notifications) return null; + const { mutate: updateReaded } = useMutation({ + mutationFn: () => updateReadedNotification(), + onError: (error: ReturnError) => { + const code = error.code as Parameters< + typeof m.backend_message + >[0]['code']; + toast.error(m.backend_message({ code }), { + richColors: true, + }); + }, + }); + + const onOpenNotification = (isOpen: boolean) => { + _setOpen(isOpen); + updateReaded(); + }; + + useEffect(() => { + if (data) { + setHasNew(data.hasNewNotify); + } + }, [data]); + + if (!data) return null; return ( - +