import { data, useFetcher } from "react-router"; import { Link } from "react-router"; import { useTranslation } from "react-i18next"; import type { Route } from "./+types/notifications"; import { ClientDate } from "~/components/ClientDate"; import { loadNotifications } from "./notifications.server"; export async function loader({ request }: Route.LoaderArgs) { return data(await loadNotifications(request)); } export function meta(_args: Route.MetaArgs) { return [{ title: "Notifications — trails.cool" }]; } interface NotificationRow { id: string; type: string; readAt: string | null; createdAt: string; link: string; payload: Record | null; } interface RequestRow { id: string; followerUsername: string; followerDisplayName: string | null; followerDomain: string; createdAt: string; } function summary(t: (key: string, opts?: Record) => string, n: NotificationRow): string { const p = n.payload as { followerUsername?: string; followerDisplayName?: string | null; targetUsername?: string; targetDisplayName?: string | null; activityName?: string; ownerUsername?: string; ownerDisplayName?: string | null } | null; const someone = t("notifications.someone"); switch (n.type) { case "follow_request_received": { const name = p?.followerDisplayName ?? p?.followerUsername ?? someone; return t("notifications.summary.followRequestReceived", { name }); } case "follow_received": { const name = p?.followerDisplayName ?? p?.followerUsername ?? someone; return t("notifications.summary.followReceived", { name }); } case "follow_request_approved": { const name = p?.targetDisplayName ?? p?.targetUsername ?? someone; return t("notifications.summary.followRequestApproved", { name }); } case "activity_published": { const owner = p?.ownerDisplayName ?? p?.ownerUsername ?? someone; const activity = p?.activityName ?? ""; return t("notifications.summary.activityPublished", { owner, activity }); } default: return n.type; } } function NotificationItem({ row }: { row: NotificationRow }) { const { t } = useTranslation("journal"); const fetcher = useFetcher(); const inFlight = fetcher.state !== "idle"; const onClick = (e: React.MouseEvent) => { if (row.readAt) return; // already read; let the link navigate normally // Mark read in the background; navigation proceeds via the anchor. fetcher.submit(null, { method: "post", action: `/api/notifications/${row.id}/read`, }); void e; }; return (
  • {!row.readAt && (
    {inFlight && marking read}
  • ); } function RequestItem({ row }: { row: RequestRow }) { const { t } = useTranslation("journal"); const approve = useFetcher(); const reject = useFetcher(); const inFlight = approve.state !== "idle" || reject.state !== "idle"; return (
  • {row.followerDisplayName ?? row.followerUsername}

    @{row.followerUsername}@{row.followerDomain} ·{" "}

  • ); } function TabLink({ to, active, label, badge, }: { to: string; active: boolean; label: string; badge: number; }) { return ( {label} {badge > 0 && ( {badge} )} ); } export default function Notifications({ loaderData }: Route.ComponentProps) { const { tab, pendingCount, notifications, nextCursor, requests } = loaderData; const { t } = useTranslation("journal"); const markAll = useFetcher(); const hasUnread = notifications.some((n) => !n.readAt); return (

    {t("notifications.title")}

    {tab === "activity" && hasUnread && ( )}
    {tab === "activity" ? ( notifications.length === 0 ? (

    {t("notifications.empty")}

    ) : ( <>
      {notifications.map((n) => ( ))}
    {nextCursor && (
    {t("notifications.loadOlder")}
    )} ) ) : requests.length === 0 ? (

    {t("social.requests.empty")}

    ) : (
      {requests.map((r) => ( ))}
    )}
    ); }