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:
@@ -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,31 +101,37 @@ 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(
|
||||||
setLoading(true)
|
async function loadNotifications() {
|
||||||
setLoadingError(null)
|
setLoading(true)
|
||||||
|
setLoadingError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await requestWithAuth(() => listNotifications({ offset: 0, limit: 50 }), {
|
const res = await requestWithAuth(() => listNotifications({ offset: 0, limit: 50 }), {
|
||||||
onUnauthorized: () => loadNotifications(),
|
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)
|
setLoading(false)
|
||||||
return
|
} catch (err: unknown) {
|
||||||
|
if (!mountedRef.current) return
|
||||||
|
setLoading(false)
|
||||||
|
setLoadingError(toApiError(err).msg)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
setNotifications(res)
|
[setStoreNotifications],
|
||||||
setLoading(false)
|
)
|
||||||
} catch (err: unknown) {
|
|
||||||
if (!mountedRef.current) return
|
|
||||||
setLoading(false)
|
|
||||||
setLoadingError(toApiError(err).msg)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mountedRef.current = true
|
mountedRef.current = true
|
||||||
@@ -134,32 +141,36 @@ export default function NotificationsPage() {
|
|||||||
}
|
}
|
||||||
}, [loadNotifications])
|
}, [loadNotifications])
|
||||||
|
|
||||||
const markAllAsRead = useCallback(async function markAllAsRead() {
|
const markAllAsRead = useCallback(
|
||||||
if (markAllPendingRef.current) return
|
async function markAllAsRead() {
|
||||||
|
if (markAllPendingRef.current) return
|
||||||
|
|
||||||
markAllPendingRef.current = true
|
markAllPendingRef.current = true
|
||||||
setMarkingAll(true)
|
setMarkingAll(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await requestWithAuth(() => markAllNotificationsAsRead(), {
|
const res = await requestWithAuth(() => markAllNotificationsAsRead(), {
|
||||||
onUnauthorized: () => markAllAsRead(),
|
onUnauthorized: () => markAllAsRead(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!mountedRef.current) return
|
if (!mountedRef.current) return
|
||||||
|
|
||||||
if (res === null) {
|
if (res === null) {
|
||||||
return
|
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 })))
|
[markStoreAllRead],
|
||||||
} catch (err: unknown) {
|
)
|
||||||
if (!mountedRef.current) return
|
|
||||||
notifyInfo(toApiError(err).msg)
|
|
||||||
} finally {
|
|
||||||
markAllPendingRef.current = false
|
|
||||||
if (mountedRef.current) setMarkingAll(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const unreadCount = useMemo(
|
const unreadCount = useMemo(
|
||||||
() => notifications.filter((notification) => !notification.read).length,
|
() => notifications.filter((notification) => !notification.read).length,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 })),
|
},
|
||||||
})),
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user