"use client"; /** * /notifications — the signed-in user's notification feed. * * Lists everything newest-first and marks the whole feed read on open * (fire-and-forget), so the header bell's unread dot clears after a * visit. Each row links to the actor's profile, where follow requests can * be accepted/rejected. * * Anonymous visitors are bounced through the auth gate (then home if they * dismiss it), mirroring /u/me. */ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { Bell } from "lucide-react"; import { useAuth } from "@/components/auth/auth-provider"; import { useAuthGate } from "@/components/auth/auth-gate"; import { useNotifications } from "@/components/notifications/notifications-provider"; import { notifications, type AppNotification } from "@/lib/social"; import { UserAvatar } from "@/components/ui/user-avatar"; import { timeAgo } from "@/lib/time"; export default function NotificationsPage() { const { user, loading } = useAuth(); const { requireAuth } = useAuthGate(); const { lastEventAt, clearUnread } = useNotifications(); const router = useRouter(); const [items, setItems] = useState([]); const [state, setState] = useState<"loading" | "ready" | "error">("loading"); // Auth gate — same pattern as /u/me. useEffect(() => { if (loading || user) return; let cancelled = false; (async () => { const ok = await requireAuth(); if (!ok && !cancelled) router.replace("/"); })(); return () => { cancelled = true; }; }, [loading, user, requireAuth, router]); // Load the feed + mark everything read once we have a user, and again // whenever a realtime notification arrives while the page is open // (lastEventAt bumps). Marking read here also clears the header badge. useEffect(() => { if (!user) return; let cancelled = false; notifications .list() .then((rows) => { if (!cancelled) { setItems(rows ?? []); setState("ready"); } }) .catch(() => { if (!cancelled) setState("error"); }); notifications .markAllRead() .then(() => clearUnread()) .catch(() => { /* non-critical */ }); return () => { cancelled = true; }; }, [user, lastEventAt, clearUnread]); if (loading || !user) { return (

{loading ? "Loading…" : "Sign in to view your notifications."}

); } return (

Notifications

{state === "error" ? (

Could not load notifications. Please try again.

) : state === "loading" ? (

Loading…

) : items.length === 0 ? ( ) : (
    {items.map((n) => ( ))}
)}
); } function NotificationRow({ n, selfHandle, }: { n: AppNotification; selfHandle: string; }) { // admin_message rows have no actor — they're a direct message from the team. const isAdmin = n.kind === "admin_message"; const display = isAdmin ? "Prabhupāda Says" : n.actor_name || (n.actor_handle ? `@${n.actor_handle}` : "Someone"); const message = messageFor(n); const unread = !n.read_at; const body = (
{isAdmin ? (

{display} {n.body ? {n.body} : null}

) : (

{display} {message}

)}

{timeAgo(n.created_at)}

{unread ? ( ) : null}
); // A like links to your own post; a mention links to the actor's post you // were tagged in; everything else links to the actor's profile. const href = n.kind === "post_like" && n.post_id ? `/u/${selfHandle}/posts#post-${n.post_id}` : n.kind === "post_mention" && n.post_id && n.actor_handle ? `/u/${n.actor_handle}/posts#post-${n.post_id}` : (n.kind === "post_comment" || n.kind === "comment_reply" || n.kind === "comment_mention") && n.post_id && n.post_author_handle ? `/u/${n.post_author_handle}/posts#post-${n.post_id}` : n.actor_handle ? `/u/${n.actor_handle}` : null; return href ? (
  • {body}
  • ) : (
  • {body}
  • ); } function cnRow(unread: boolean) { return [ "flex items-start gap-3 px-4 py-3 transition-colors hover:bg-muted/50", unread ? "bg-primary/5" : "", ].join(" "); } /** Human-readable tail for each notification kind. Add cases as new * notification kinds are introduced on the backend. */ function messageFor(n: AppNotification): string { switch (n.kind) { case "follow_request": return "requested to follow you."; case "follow_accepted": return "accepted your follow request."; case "post_like": { const others = (n.actor_count ?? 1) - 1; if (others <= 0) return "liked your post."; return `and ${others} ${others === 1 ? "other" : "others"} liked your post.`; } case "post_mention": return "mentioned you in a post."; case "post_comment": return "commented on your post."; case "comment_reply": return "replied to your comment."; case "comment_mention": return "mentioned you in a comment."; case "admin_message": return n.body ?? "sent you a message."; default: return "sent you a notification."; } } function Empty() { return (

    No notifications yet.

    ); }