'use client'; import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react'; import Link from 'next/link'; import { BellIcon, SettingsIcon } from 'lucide-react'; import { authClient } from '@/lib/auth/client'; import { Badge } from '@/components/ui/badge'; 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'; type Item = { id: string; title: string; body: string; url: string; readAt: 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 () => {}, 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) => { 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(''); } 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 [open, setOpen] = useState(false); return { setOpen(value); if (value) void state.refresh(); }}> }> {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.readAt && ●}{item.title}

{item.body}

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

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

} {state.error &&

{state.error}

} {state.nextCursor && }
}
; }