"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 Image from "next/image"; 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"; 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 }: { n: AppNotification }) { const display = n.actor_name || (n.actor_handle ? `@${n.actor_handle}` : "Someone"); const message = messageFor(n.kind); const unread = !n.read_at; const body = (

{display} {message}

{timeAgo(n.created_at)}

{unread ? ( ) : null}
); // Link to the actor's profile when we know their handle. return n.actor_handle ? (
  • {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(kind: string): string { switch (kind) { case "follow_request": return "requested to follow you."; case "follow_accepted": return "accepted your follow request."; default: return "sent you a notification."; } } function Avatar({ src, name }: { src: string | null; name: string }) { if (src) { return ( {name} ); } const initials = name .replace(/^@/, "") .split(/\s+/) .map((p) => p[0]) .slice(0, 2) .join("") .toUpperCase() || "?"; return (
    {initials}
    ); } function Empty() { return (

    No notifications yet.

    ); } /** Compact relative time ("just now", "5m", "3h", "2d"), falling back to * a date for anything older than a week. */ function timeAgo(iso: string): string { const then = new Date(iso).getTime(); if (Number.isNaN(then)) return ""; const secs = Math.max(0, Math.floor((Date.now() - then) / 1000)); if (secs < 45) return "just now"; const mins = Math.floor(secs / 60); if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 7) return `${days}d ago`; return new Date(iso).toLocaleDateString(); }