import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useRef, useState, type FormEvent } from "react"; import { Link, useLocation } from "wouter"; import { api, type EntryView } from "../api.ts"; import { Cover } from "../components/Cover.tsx"; import { EntryDetail } from "../components/EntryDetail.tsx"; import { Icon } from "../components/Icon.tsx"; import { describeRating, formatRating, friendlyError, mediaTypeLabel, rollupLabel, statusLabel, type Status } from "../format.ts"; import { useDebounced, usePendingChanges } from "../hooks.ts"; import { useRatingScale } from "../settings.ts"; const TYPE_FILTERS = ["book", "manga", "comic", "game", "movie", "show", "podcast", "audio_drama", "album", "artist", "video", "other"]; const STATUS_FILTERS: Status[] = ["in_progress", "planned", "completed", "paused", "dropped"]; /** The library: list pane (search, quick add, filters, rows) and the detail pane. */ export function Library({ selectedId }: { selectedId: string | null }) { return (
{selectedId ? ( ) : (

Nothing selected Pick something from your library, or quick add a title and sort out the details later.

)}
); } // Filters survive navigating between entries (the list pane remounts on route change). type Filters = { q: string; mediaType?: string; status?: Status; draft: boolean }; let savedFilters: Filters = { q: "", draft: false }; /** Forget per-session library state (on sign-out). */ export function resetLibraryState(): void { savedFilters = { q: "", draft: false }; } function ListPane({ selectedId }: { selectedId: string | null }) { const [, navigate] = useLocation(); const qc = useQueryClient(); const scale = useRatingScale(); const [filters, setFiltersState] = useState(savedFilters); const setFilters = (f: typeof filters) => { savedFilters = f; setFiltersState(f); }; const q = useDebounced(filters.q.trim()); const query = { q: q || undefined, mediaType: filters.mediaType, status: filters.status, draft: filters.draft || undefined }; const entries = useInfiniteQuery({ queryKey: ["entries", query], queryFn: ({ pageParam }) => api.entries({ ...query, cursor: pageParam, limit: 50 }), initialPageParam: undefined as string | undefined, getNextPageParam: (last) => last.nextCursor ?? undefined, }); const changes = usePendingChanges(); const flagged = new Set((changes.data?.items ?? []).filter((c) => c.targetEntity === "entry").map((c) => c.targetId)); const [quickError, setQuickError] = useState(null); const quickInput = useRef(null); const quick = useMutation({ mutationFn: api.quickAdd, onSuccess: (entry, _t) => { qc.setQueryData(["entry", entry.id], entry); void qc.invalidateQueries({ queryKey: ["entries"] }); navigate(`/entries/${entry.id}`); if (quickInput.current) quickInput.current.value = ""; quickInput.current?.focus(); }, onError: (e) => setQuickError(friendlyError(e)), }); function onQuick(e: FormEvent) { e.preventDefault(); const title = (quickInput.current?.value ?? "").trim(); if (!title || quick.isPending) return; setQuickError(null); quick.mutate(title); } const items = entries.data?.pages.flatMap((p) => p.items) ?? []; const filtering = Boolean(q || filters.mediaType || filters.status || filters.draft); return (
e.preventDefault()}> setFilters({ ...filters, q: e.target.value })} />
{quickError && }
{STATUS_FILTERS.map((s) => ( ))}
{TYPE_FILTERS.map((t) => ( ))}
{entries.isPending ? (
    {[0, 1, 2, 3].map((i) => ( ))}
) : entries.error ? (

{friendlyError(entries.error)}

) : items.length === 0 ? (
{filtering ? ( <>

Nothing matches these filters.

) : (

Your library is empty. Type a title into Quick add and press Enter. A title alone is enough; you can link it to a catalogue entry later.

)}
) : (
    {items.map((e) => (
  • ))} {entries.hasNextPage && (
  • )}
)}
); } /** `media type · meta`: children roll-up, else status, plus the year. */ export function rowMeta(e: EntryView): string { const type = e.mediaTypeOverride ?? e.work.mediaType; const parts = [mediaTypeLabel(type)]; const rollup = rollupLabel(type, e.childSummary); if (rollup) parts.push(rollup); else if (e.status !== "none") parts.push(statusLabel(e.status, type).toLowerCase()); if (e.work.year) parts.push(String(e.work.year)); return parts.join(" · "); } function Row({ entry: e, selected, flagged, scale }: { entry: EntryView; selected: boolean; flagged: boolean; scale: ReturnType }) { return (
{e.work.title}
{rowMeta(e)}
{flagged && } {e.draft ? ( Draft ) : e.rating !== null ? ( {formatRating(e.rating, scale)} ) : e.status === "in_progress" ? ( ) : null} ); }