diff --git a/app/narrative-discovery/page.tsx b/app/narrative-discovery/page.tsx index f2eddd1..9c16171 100644 --- a/app/narrative-discovery/page.tsx +++ b/app/narrative-discovery/page.tsx @@ -8,6 +8,8 @@ import type { NarrativeListResponse, NarrativeItem } from "@/lib/types/narrative import { stashNarrativeHandoff, clearNarrativeHandoff, useNarrativeHandoff } from "@/lib/narrativeHandoff"; type RiskLevel = "High" | "Medium" | "Low"; +const NARRATIVES_PER_PAGE_OPTIONS = [6, 9, 12, 18] as const; +type NarrativesPerPage = (typeof NARRATIVES_PER_PAGE_OPTIONS)[number]; // Helpers function riskBadgeClasses(level: RiskLevel) { @@ -19,6 +21,20 @@ function riskBadgeClasses(level: RiskLevel) { } } +function getNarrativeBodyText(narrative: NarrativeItem) { + const description = narrative.description?.trim(); + const detail = narrative.detail?.trim(); + + if (!detail) return description || "No description available."; + + // Some backend rows send a raw JSON blob in `detail`; don't show that in the modal. + if (detail.startsWith("{") || detail.startsWith("[")) { + return description || "No description available."; + } + + return detail || description || "No description available."; +} + type SortOption = "trending" | "risk" | "views"; export default function NarrativeDiscoveryPage() { @@ -39,6 +55,8 @@ function NarrativeDiscoveryInner() { const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const [sortBy, setSortBy] = useState("trending"); + const [page, setPage] = useState(1); + const [narrativesPerPage, setNarrativesPerPage] = useState(6); // Debounce search input useEffect(() => { @@ -58,6 +76,7 @@ function NarrativeDiscoveryInner() { }); if (!cancelled) { setNarratives(response.data); + setPage(1); setError(null); } } catch (err) { @@ -91,6 +110,14 @@ function NarrativeDiscoveryInner() { } const visibleNarratives = narratives; + const totalPages = Math.max(1, Math.ceil(visibleNarratives.length / narrativesPerPage)); + const safePage = Math.min(page, totalPages); + const pageNarratives = useMemo(() => { + const start = (safePage - 1) * narrativesPerPage; + return visibleNarratives.slice(start, start + narrativesPerPage); + }, [narrativesPerPage, safePage, visibleNarratives]); + const rangeStart = visibleNarratives.length === 0 ? 0 : (safePage - 1) * narrativesPerPage + 1; + const rangeEnd = visibleNarratives.length === 0 ? 0 : Math.min(visibleNarratives.length, rangeStart + pageNarratives.length - 1); if (error) { return ( @@ -125,7 +152,10 @@ function NarrativeDiscoveryInner() { type="search" placeholder="Search narratives..." value={search} - onChange={(e) => setSearch(e.target.value)} + onChange={(e) => { + setSearch(e.target.value); + setPage(1); + }} className="h-10 w-full rounded-lg border border-zinc-200 bg-white pl-9 pr-3 text-sm text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-900 focus:outline-none focus:ring-2 focus:ring-zinc-900/10 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500 dark:focus:border-zinc-400 dark:focus:ring-zinc-400/20" /> @@ -134,7 +164,10 @@ function NarrativeDiscoveryInner() { Sort by { + setNarrativesPerPage(Number(e.target.value) as NarrativesPerPage); + setPage(1); + }} + className="cursor-pointer rounded-lg border border-zinc-200 bg-white px-2.5 py-1.5 text-xs font-medium text-zinc-800 shadow-sm focus:border-zinc-900 focus:outline-none focus:ring-2 focus:ring-zinc-900/10 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100 dark:focus:border-zinc-400 dark:focus:ring-zinc-400/20" + > + {NARRATIVES_PER_PAGE_OPTIONS.map((n) => ( + + ))} + + +
+ + + Page {safePage} / {totalPages} + + +
+ + + )} {activeNarrative && ( @@ -208,6 +306,7 @@ function useIsClient() { function NarrativeDetailDialog({ narrative, onClose }: { narrative: NarrativeItem; onClose: () => void }) { const isClient = useIsClient(); + const narrativeBodyText = getNarrativeBodyText(narrative); useEffect(() => { const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); @@ -242,7 +341,7 @@ function NarrativeDetailDialog({ narrative, onClose }: { narrative: NarrativeIte -

{narrative.detail || narrative.description}

+

{narrativeBodyText}