"use client"; import { useCallback, useEffect, useState } from "react"; import Link from "next/link"; import { useTranslations } from "next-intl"; import { Bell } from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/shared/empty-state"; import { cn } from "@/lib/utils"; import { notificationHref, type NotificationType } from "@/lib/notification-links"; const PAGE_SIZE = 20; type Notification = { id: string; type: NotificationType; recipeId: string | null; commentId: string | null; read: boolean; createdAt: string; actorId: string; actorName: string; actorUsername: string | null; actorAvatarUrl: string | null; }; type NotificationsResponse = { notifications: Notification[]; unreadCount: number; total: number; limit: number; offset: number; }; function timeAgo(dateStr: string, t: ReturnType) { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return t("justNow"); if (mins < 60) return t("minutesAgo", { mins }); const hours = Math.floor(mins / 60); if (hours < 24) return t("hoursAgo", { hours }); return t("daysAgo", { days: Math.floor(hours / 24) }); } export function NotificationsPageContent() { const t = useTranslations("notifications"); const tSocial = useTranslations("social"); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [unreadCount, setUnreadCount] = useState(0); const [offset, setOffset] = useState(0); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(false); const fetchPage = useCallback(async (off: number, append: boolean) => { if (append) setLoadingMore(true); else setLoading(true); setError(false); try { const res = await fetch(`/api/v1/notifications?limit=${PAGE_SIZE}&offset=${off}`); if (!res.ok) throw new Error("Request failed"); const json = (await res.json()) as NotificationsResponse; setItems((prev) => (append ? [...prev, ...json.notifications] : json.notifications)); setTotal(json.total); setUnreadCount(json.unreadCount); setOffset(off + json.notifications.length); } catch { setError(true); } finally { setLoading(false); setLoadingMore(false); } }, []); useEffect(() => { void fetchPage(0, false); }, [fetchPage]); async function markRead(id: string) { setItems((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n))); setUnreadCount((prev) => { const n = items.find((x) => x.id === id); return n && !n.read ? Math.max(0, prev - 1) : prev; }); await fetch(`/api/v1/notifications/${id}/read`, { method: "POST" }); } async function markAllRead() { setUnreadCount(0); setItems((prev) => prev.map((n) => ({ ...n, read: true }))); await fetch("/api/v1/notifications/read", { method: "POST" }); } const hasMore = items.length < total; return (

{t("title")}

{unreadCount > 0 && ( )}
{loading ? (

{t("loading")}

) : error && items.length === 0 ? (

{t("loadFailed")}

) : items.length === 0 ? ( ) : (
{items.map((n) => ( { if (!n.read) void markRead(n.id); }} className={cn( "flex items-start gap-3 p-4 hover:bg-accent transition-colors", !n.read && "bg-accent/40" )} > {n.actorName.slice(0, 2).toUpperCase()}

{t(n.type, { name: n.actorName })}

{timeAgo(n.createdAt, tSocial)}

{!n.read && } ))}
)} {error && items.length > 0 && (

{t("loadFailed")}

)} {(hasMore || (error && items.length > 0)) && (
)}
); }