"use client"; import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode, } from "react"; import Link from "next/link"; import Image from "@/components/ui/resilient-image"; import { BellIcon, BellOffIcon, SettingsIcon } from "lucide-react"; import { authClient } from "@/lib/auth/client"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { useSitePushDisabled } from "./push-client"; type Item = { id: string; title: string; body: string; url: string; readAt: string | null; authorName: string | null; authorImage: string | null; imageUrl: string | null; }; type Feed = { items: Item[]; unread: number; nextCursor: string | null }; const empty: Feed = { items: [], unread: 0, nextCursor: null }; type State = Feed & { signedIn: boolean; busy: boolean; error: string; refresh: (more?: boolean) => Promise; read: (id?: string) => Promise; }; const Context = createContext({ ...empty, signedIn: false, busy: false, error: "", refresh: async () => undefined, read: async () => {}, }); export function NotificationProvider({ children }: { children: ReactNode }) { const { data } = authClient.useSession(); const userId = data?.user.id; const [feed, setFeed] = useState(empty); const [owner, setOwner] = useState(undefined); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const current = useRef(feed); useEffect(() => { current.current = feed; }, [feed]); const sequence = useRef(0); const invalidate = useCallback(() => { ++sequence.current; }, []); const refresh = useCallback( async (more = false): Promise => { if (!userId) return; const request = ++sequence.current; const cursor = more ? current.current.nextCursor : null; setBusy(true); try { const response = await fetch( `/api/notifications${cursor ? `?before=${encodeURIComponent(cursor)}` : ""}`, { cache: "no-store" }, ); if (!response.ok) throw new Error("โหลดการแจ้งเตือนไม่สำเร็จ"); const result = (await response.json()) as Feed; if (sequence.current !== request) return; setFeed((previous) => ({ ...result, items: more ? [ ...previous.items, ...result.items.filter( (item) => !previous.items.some((old) => old.id === item.id), ), ] : result.items, })); setOwner(userId); setError(""); return result; } catch (cause) { if (sequence.current === request) setError( cause instanceof Error ? cause.message : "โหลดการแจ้งเตือนไม่สำเร็จ", ); } finally { if (sequence.current === request) setBusy(false); } }, [userId], ); useEffect(() => { if (!userId) return; void Promise.resolve().then(() => refresh()); const source = new EventSource("/api/notifications/events"); source.addEventListener("changed", () => void refresh()); source.addEventListener("open", () => void refresh()); return () => { invalidate(); source.close(); }; }, [userId, refresh, invalidate]); const read = useCallback( async (id?: string) => { if (!userId) return; setFeed((previous) => ({ ...previous, unread: id ? Math.max( 0, previous.unread - (previous.items.some((item) => item.id === id && !item.readAt) ? 1 : 0), ) : 0, items: previous.items.map((item) => !id || item.id === id ? { ...item, readAt: new Date().toISOString() } : item, ), })); try { const response = await fetch("/api/notifications", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(id ? { id } : {}), }); if (!response.ok) throw new Error("บันทึกการอ่านไม่สำเร็จ"); } finally { await refresh(); } }, [userId, refresh], ); return ( {children} ); } export function NotificationBell() { const state = useContext(Context); const disabled = useSitePushDisabled(); const [open, setOpen] = useState(false); return ( { setOpen(value); if (value) { if (state.unread > 0) void state.read().catch(() => undefined); else void state.refresh().then((result) => { if (result?.unread) void state.read().catch(() => undefined); }); } }} > } > {disabled ? : } {!disabled && state.unread > 0 && ( {state.unread > 9 ? "9+" : state.unread} )}

การแจ้งเตือน

{!state.signedIn ? ( เข้าสู่ระบบเพื่อดูการแจ้งเตือน ) : ( <> {state.unread > 0 && ( )}
{state.items.map((item) => ( { setOpen(false); void state.read(item.id).catch(() => undefined); }} > {item.imageUrl ? ( ) : ( (item.authorName || item.authorImage) && ( {item.authorName?.slice(0, 2) || "?"} ) )}

{!item.readAt && ( ● )} {item.title}

{item.body}

))} {state.busy && !state.items.length && (
)} {!state.busy && !state.items.length && !state.error && (

ยังไม่มีการแจ้งเตือน

)} {state.error && (

{state.error}

)} {state.nextCursor && ( )}
)}
); }