"use client"; import type { EventDTO, WatchlistItem } from "@dci/core"; import { RefreshCw } from "lucide-react"; import Link from "next/link"; import { useCallback, useEffect, useState } from "react"; import { EventList } from "@/components/event-list"; import { WatchButton } from "@/components/intelligence/watch-button"; import { Button } from "@/components/ui/button"; import { Metric, MetricCell, MetricRow } from "@/components/ui/metric"; import { SkeletonRows } from "@/components/ui/skeleton"; import { EmptyState, ErrorState } from "@/components/ui/states"; import { clientApiBase } from "@/lib/api"; import { fmtInt } from "@/lib/format"; import { ENTITY_TYPE_LABEL } from "@/lib/labels"; import { entityHref, routes } from "@/lib/routes"; const PER_PAGE = 50; const TYPE_ORDER: WatchlistItem["entityType"][] = ["operator", "metro", "country", "project", "facility"]; /** Cookie-scoped call (credentials included) — apiGet has no credentials option. */ async function get(path: string): Promise<{ ok: boolean; data: T | null; total: number; error: string | null }> { try { const res = await fetch(`${clientApiBase()}${path}`, { credentials: "include", headers: { accept: "application/json" }, cache: "no-store" }); if (!res.ok) return { ok: false, data: null, total: 0, error: `HTTP ${res.status}` }; const j = (await res.json()) as { data: T; meta?: { total?: number } }; return { ok: true, data: j.data, total: j.meta?.total ?? 0, error: null }; } catch (e) { return { ok: false, data: null, total: 0, error: e instanceof Error ? e.message : "network error" }; } } interface Fetched { ok: boolean; items: WatchlistItem[]; feed: EventDTO[]; feedTotal: number; error: string | null; } /** * The watchlist is keyed by an httpOnly cookie on the API origin, so it is read in the browser with * credentials — the server never sees it. Items + feed load together; unfollow refreshes both. */ async function fetchAll(): Promise { const [a, b] = await Promise.all([get("/watchlist"), get(`/watchlist/feed?per_page=${PER_PAGE}`)]); return { ok: a.ok, items: a.data ?? [], feed: b.data ?? [], feedTotal: b.total || (b.data?.length ?? 0), error: a.ok ? null : a.error }; } export function WatchlistView() { const [state, setState] = useState(null); const [more, setMore] = useState(false); const [tick, setTick] = useState(0); const load = useCallback(() => setTick((t) => t + 1), []); useEffect(() => { let alive = true; void fetchAll().then((s) => { if (alive) setState(s); }); return () => { alive = false; }; }, [tick]); const loadMore = async () => { if (!state) return; setMore(true); const page = Math.floor(state.feed.length / PER_PAGE) + 1; const r = await get(`/watchlist/feed?page=${page}&per_page=${PER_PAGE}`); setMore(false); if (r.ok && Array.isArray(r.data)) setState({ ...state, feed: [...state.feed, ...r.data.filter((e) => !state.feed.some((x) => x.id === e.id))] }); }; if (!state) return ; if (!state.ok) return Retry} />; const groups = TYPE_ORDER.map((t) => ({ type: t, items: state.items.filter((i) => i.entityType === t) })).filter((g) => g.items.length); if (state.items.length === 0) { return ( } /> ); } return (

Watched

{groups.map((g) => (

{ENTITY_TYPE_LABEL[g.type] ?? g.type}

{fmtInt(g.items.length)}
    {g.items.map((it) => (
  • {it.name}
  • ))}
))}

Watchlist feed

Newest first · deduplicated per announcement
{state.feed.length > 0 && state.feed.length < state.feedTotal && (
)}
); }