refactor(notifications): fetch from backend API instead of local generation

Rewrite store/notifications.ts to fetch via listNotifications
API and remove local generateId-based notification creation.
The store now acts as a simple cache with fetch/invalidate methods.
Header unread count reads from this API-backed cache.
This commit is contained in:
zetaloop
2026-05-01 04:21:03 +08:00
parent d76866ac3b
commit cd469d3d54
4 changed files with 84 additions and 74 deletions
+15 -4
View File
@@ -10,6 +10,7 @@ import { toApiError } from "@/lib/errors"
import { notifyInfo } from "@/lib/toast" import { notifyInfo } from "@/lib/toast"
import type { Notification } from "@/lib/types" import type { Notification } from "@/lib/types"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useNotificationStore } from "@/store/notifications"
import { Bell, CheckCheck, Loader2, MessageSquare, ShoppingBag } from "lucide-react" import { Bell, CheckCheck, Loader2, MessageSquare, ShoppingBag } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
@@ -100,8 +101,11 @@ export default function NotificationsPage() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [loadingError, setLoadingError] = useState<string | null>(null) const [loadingError, setLoadingError] = useState<string | null>(null)
const [markingAll, setMarkingAll] = useState(false) const [markingAll, setMarkingAll] = useState(false)
const setStoreNotifications = useNotificationStore((state) => state.setNotifications)
const markStoreAllRead = useNotificationStore((state) => state.markAllRead)
const loadNotifications = useCallback(async function loadNotifications() { const loadNotifications = useCallback(
async function loadNotifications() {
setLoading(true) setLoading(true)
setLoadingError(null) setLoadingError(null)
@@ -118,13 +122,16 @@ export default function NotificationsPage() {
} }
setNotifications(res) setNotifications(res)
setStoreNotifications(res)
setLoading(false) setLoading(false)
} catch (err: unknown) { } catch (err: unknown) {
if (!mountedRef.current) return if (!mountedRef.current) return
setLoading(false) setLoading(false)
setLoadingError(toApiError(err).msg) setLoadingError(toApiError(err).msg)
} }
}, []) },
[setStoreNotifications],
)
useEffect(() => { useEffect(() => {
mountedRef.current = true mountedRef.current = true
@@ -134,7 +141,8 @@ export default function NotificationsPage() {
} }
}, [loadNotifications]) }, [loadNotifications])
const markAllAsRead = useCallback(async function markAllAsRead() { const markAllAsRead = useCallback(
async function markAllAsRead() {
if (markAllPendingRef.current) return if (markAllPendingRef.current) return
markAllPendingRef.current = true markAllPendingRef.current = true
@@ -152,6 +160,7 @@ export default function NotificationsPage() {
} }
setNotifications((prev) => prev.map((n) => (n.read ? n : { ...n, read: true }))) setNotifications((prev) => prev.map((n) => (n.read ? n : { ...n, read: true })))
markStoreAllRead()
} catch (err: unknown) { } catch (err: unknown) {
if (!mountedRef.current) return if (!mountedRef.current) return
notifyInfo(toApiError(err).msg) notifyInfo(toApiError(err).msg)
@@ -159,7 +168,9 @@ export default function NotificationsPage() {
markAllPendingRef.current = false markAllPendingRef.current = false
if (mountedRef.current) setMarkingAll(false) if (mountedRef.current) setMarkingAll(false)
} }
}, []) },
[markStoreAllRead],
)
const unreadCount = useMemo( const unreadCount = useMemo(
() => notifications.filter((notification) => !notification.read).length, () => notifications.filter((notification) => !notification.read).length,
+8 -1
View File
@@ -34,7 +34,7 @@ import {
} from "lucide-react" } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { usePathname, useRouter } from "next/navigation" import { usePathname, useRouter } from "next/navigation"
import { useState } from "react" import { useEffect, useState } from "react"
import { canAccessDashboard } from "@/components/role-guard" import { canAccessDashboard } from "@/components/role-guard"
@@ -59,6 +59,12 @@ export function Header() {
} = useAuthStore() } = useAuthStore()
const canOpenDashboard = currentRole === "player" || currentRole === "owner" const canOpenDashboard = currentRole === "player" || currentRole === "owner"
const { shop: ownerShop } = useMyShop(isAuthenticated && 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 = const navLinks =
currentRole === "consumer" currentRole === "consumer"
@@ -110,6 +116,7 @@ export function Header() {
notifyInfo(toApiError(error).msg) notifyInfo(toApiError(error).msg)
} finally { } finally {
clearAuth() clearAuth()
useNotificationStore.getState().invalidate()
setMobileOpen(false) setMobileOpen(false)
router.push("/") router.push("/")
} }
+5 -1
View File
@@ -8,7 +8,11 @@ export { sendEmailVerificationCode } from "./email"
export { isFavorited, listFavorites } from "./favorites" export { isFavorited, listFavorites } from "./favorites"
export { getFileById, uploadFile } from "./files" export { getFileById, uploadFile } from "./files"
export { getGameById, listGames } from "./games" export { getGameById, listGames } from "./games"
export { listNotifications, markNotificationAsRead } from "./notifications" export {
listNotifications,
markAllNotificationsAsRead,
markNotificationAsRead,
} from "./notifications"
export { getOrderById, listOrders, listOrdersByConsumer } from "./orders" export { getOrderById, listOrders, listOrdersByConsumer } from "./orders"
export { getPlayerById, listPlayers, listPlayersByShop } from "./players" export { getPlayerById, listPlayers, listPlayersByShop } from "./players"
export { createPost, getPostById, listPosts, listPostsByAuthor, togglePostLike } from "./posts" export { createPost, getPostById, listPosts, listPostsByAuthor, togglePostLike } from "./posts"
+20 -32
View File
@@ -1,48 +1,36 @@
import { generateId } from "@/lib/id" import { listNotifications } from "@/lib/api/notifications"
import type { Notification } from "@/lib/types" import type { Notification } from "@/lib/types"
import { create } from "zustand" import { create } from "zustand"
interface CreateNotificationInput {
type: Notification["type"]
title: string
content: string
link?: string
}
interface NotificationState { interface NotificationState {
notifications: Notification[] notifications: Notification[]
addNotification: (input: CreateNotificationInput) => Notification loading: boolean
markAsRead: (notificationId: string) => void fetchNotifications: () => Promise<void>
markAllAsRead: () => void setNotifications: (notifications: Notification[]) => void
markAllRead: () => void
invalidate: () => void
} }
export const useNotificationStore = create<NotificationState>((set) => ({ export const useNotificationStore = create<NotificationState>((set) => ({
notifications: [], notifications: [],
addNotification: (input) => { loading: false,
const notification: Notification = { fetchNotifications: async () => {
id: generateId("notif"), set({ loading: true })
type: input.type, try {
title: input.title, const items = await listNotifications({ offset: 0, limit: 50 })
content: input.content, set({ notifications: items, loading: false })
link: input.link, } catch {
read: false, set({ loading: false })
createdAt: new Date().toISOString(),
} }
set((state) => ({
notifications: [notification, ...state.notifications],
}))
return notification
}, },
markAsRead: (notificationId) => setNotifications: (notifications) => set({ notifications }),
markAllRead: () =>
set((state) => ({ set((state) => ({
notifications: state.notifications.map((notification) => notifications: state.notifications.map((notification) =>
notification.id === notificationId ? { ...notification, read: true } : notification, notification.read ? notification : { ...notification, read: true },
), ),
})), })),
markAllAsRead: () => invalidate: () => {
set((state) => ({ set({ notifications: [] })
notifications: state.notifications.map((notification) => ({ ...notification, read: true })), },
})),
})) }))