diff --git a/app/(account)/notifications/page.tsx b/app/(account)/notifications/page.tsx index 7ef277e..f15ab6a 100644 --- a/app/(account)/notifications/page.tsx +++ b/app/(account)/notifications/page.tsx @@ -10,6 +10,7 @@ import { toApiError } from "@/lib/errors" import { notifyInfo } from "@/lib/toast" import type { Notification } from "@/lib/types" import { cn } from "@/lib/utils" +import { useNotificationStore } from "@/store/notifications" import { Bell, CheckCheck, Loader2, MessageSquare, ShoppingBag } from "lucide-react" import Link from "next/link" import { useCallback, useEffect, useMemo, useRef, useState } from "react" @@ -100,31 +101,37 @@ export default function NotificationsPage() { const [loading, setLoading] = useState(true) const [loadingError, setLoadingError] = useState(null) const [markingAll, setMarkingAll] = useState(false) + const setStoreNotifications = useNotificationStore((state) => state.setNotifications) + const markStoreAllRead = useNotificationStore((state) => state.markAllRead) - const loadNotifications = useCallback(async function loadNotifications() { - setLoading(true) - setLoadingError(null) + const loadNotifications = useCallback( + async function loadNotifications() { + setLoading(true) + setLoadingError(null) - try { - const res = await requestWithAuth(() => listNotifications({ offset: 0, limit: 50 }), { - onUnauthorized: () => loadNotifications(), - }) + try { + const res = await requestWithAuth(() => listNotifications({ offset: 0, limit: 50 }), { + onUnauthorized: () => loadNotifications(), + }) - if (!mountedRef.current) return + if (!mountedRef.current) return - if (res === null) { + if (res === null) { + setLoading(false) + return + } + + setNotifications(res) + setStoreNotifications(res) setLoading(false) - return + } catch (err: unknown) { + if (!mountedRef.current) return + setLoading(false) + setLoadingError(toApiError(err).msg) } - - setNotifications(res) - setLoading(false) - } catch (err: unknown) { - if (!mountedRef.current) return - setLoading(false) - setLoadingError(toApiError(err).msg) - } - }, []) + }, + [setStoreNotifications], + ) useEffect(() => { mountedRef.current = true @@ -134,32 +141,36 @@ export default function NotificationsPage() { } }, [loadNotifications]) - const markAllAsRead = useCallback(async function markAllAsRead() { - if (markAllPendingRef.current) return + const markAllAsRead = useCallback( + async function markAllAsRead() { + if (markAllPendingRef.current) return - markAllPendingRef.current = true - setMarkingAll(true) + markAllPendingRef.current = true + setMarkingAll(true) - try { - const res = await requestWithAuth(() => markAllNotificationsAsRead(), { - onUnauthorized: () => markAllAsRead(), - }) + try { + const res = await requestWithAuth(() => markAllNotificationsAsRead(), { + onUnauthorized: () => markAllAsRead(), + }) - if (!mountedRef.current) return + if (!mountedRef.current) return - if (res === null) { - return + if (res === null) { + return + } + + setNotifications((prev) => prev.map((n) => (n.read ? n : { ...n, read: true }))) + markStoreAllRead() + } catch (err: unknown) { + if (!mountedRef.current) return + notifyInfo(toApiError(err).msg) + } finally { + markAllPendingRef.current = false + if (mountedRef.current) setMarkingAll(false) } - - setNotifications((prev) => prev.map((n) => (n.read ? n : { ...n, read: true }))) - } catch (err: unknown) { - if (!mountedRef.current) return - notifyInfo(toApiError(err).msg) - } finally { - markAllPendingRef.current = false - if (mountedRef.current) setMarkingAll(false) - } - }, []) + }, + [markStoreAllRead], + ) const unreadCount = useMemo( () => notifications.filter((notification) => !notification.read).length, diff --git a/components/header.tsx b/components/header.tsx index 56393b7..486573a 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -34,7 +34,7 @@ import { } from "lucide-react" import Link from "next/link" import { usePathname, useRouter } from "next/navigation" -import { useState } from "react" +import { useEffect, useState } from "react" import { canAccessDashboard } from "@/components/role-guard" @@ -59,6 +59,12 @@ export function Header() { } = useAuthStore() const canOpenDashboard = currentRole === "player" || currentRole === "owner" const { shop: ownerShop } = useMyShop(isAuthenticated && currentRole === "owner") + const fetchNotifications = useNotificationStore((state) => state.fetchNotifications) + + useEffect(() => { + if (!isAuthenticated) return + void fetchNotifications() + }, [fetchNotifications, isAuthenticated]) const navLinks = currentRole === "consumer" @@ -110,6 +116,7 @@ export function Header() { notifyInfo(toApiError(error).msg) } finally { clearAuth() + useNotificationStore.getState().invalidate() setMobileOpen(false) router.push("/") } diff --git a/lib/api/index.ts b/lib/api/index.ts index c74877a..06f9e25 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -8,7 +8,11 @@ export { sendEmailVerificationCode } from "./email" export { isFavorited, listFavorites } from "./favorites" export { getFileById, uploadFile } from "./files" export { getGameById, listGames } from "./games" -export { listNotifications, markNotificationAsRead } from "./notifications" +export { + listNotifications, + markAllNotificationsAsRead, + markNotificationAsRead, +} from "./notifications" export { getOrderById, listOrders, listOrdersByConsumer } from "./orders" export { getPlayerById, listPlayers, listPlayersByShop } from "./players" export { createPost, getPostById, listPosts, listPostsByAuthor, togglePostLike } from "./posts" diff --git a/store/notifications.ts b/store/notifications.ts index c6e5663..b881d9c 100644 --- a/store/notifications.ts +++ b/store/notifications.ts @@ -1,48 +1,36 @@ -import { generateId } from "@/lib/id" +import { listNotifications } from "@/lib/api/notifications" import type { Notification } from "@/lib/types" import { create } from "zustand" -interface CreateNotificationInput { - type: Notification["type"] - title: string - content: string - link?: string -} - interface NotificationState { notifications: Notification[] - addNotification: (input: CreateNotificationInput) => Notification - markAsRead: (notificationId: string) => void - markAllAsRead: () => void + loading: boolean + fetchNotifications: () => Promise + setNotifications: (notifications: Notification[]) => void + markAllRead: () => void + invalidate: () => void } export const useNotificationStore = create((set) => ({ notifications: [], - addNotification: (input) => { - const notification: Notification = { - id: generateId("notif"), - type: input.type, - title: input.title, - content: input.content, - link: input.link, - read: false, - createdAt: new Date().toISOString(), + loading: false, + fetchNotifications: async () => { + set({ loading: true }) + try { + const items = await listNotifications({ offset: 0, limit: 50 }) + set({ notifications: items, loading: false }) + } catch { + set({ loading: false }) } - - set((state) => ({ - notifications: [notification, ...state.notifications], - })) - - return notification }, - markAsRead: (notificationId) => + setNotifications: (notifications) => set({ notifications }), + markAllRead: () => set((state) => ({ notifications: state.notifications.map((notification) => - notification.id === notificationId ? { ...notification, read: true } : notification, + notification.read ? notification : { ...notification, read: true }, ), })), - markAllAsRead: () => - set((state) => ({ - notifications: state.notifications.map((notification) => ({ ...notification, read: true })), - })), + invalidate: () => { + set({ notifications: [] }) + }, }))