"use client"; import { useMemo, useState } from "react"; import { Search, X, CheckSquare, RectangleVertical, RectangleHorizontal } from "lucide-react"; import { ActressCard, type ActressCardData } from "./ActressCard"; import { CategoryIcon } from "./CategoryIcon"; import { ActressSelectionProvider, useActressSelection } from "./ActressSelectionProvider"; import { ActressBulkBar } from "./ActressBulkBar"; import { reverseName } from "@/lib/jav/nameUtils"; import type { ActressCategory } from "@/lib/db/queries"; import { cn } from "@/lib/utils"; const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); const NON_LATIN = "#"; function bucketFor(name: string): string { const c = name.trim().slice(0, 1).toUpperCase(); return c >= "A" && c <= "Z" ? c : NON_LATIN; } interface ActressFull extends ActressCardData { altNames?: string | null; } export function ActressDirectory(props: { items: ActressFull[]; categories: ActressCategory[] }) { return ( ); } function DirectoryInner({ items, categories, }: { items: ActressFull[]; categories: ActressCategory[]; }) { const sel = useActressSelection(); const [query, setQuery] = useState(""); const [activeLetter, setActiveLetter] = useState(null); // null = ALL, "unassigned" = actresses with no categories, number = category id const [activeCategoryId, setActiveCategoryId] = useState(null); // P (default) uses portraits.p1, L uses portraits.ph (golden landscape). const [view, setView] = useState<"portrait" | "landscape">("portrait"); const builtins = useMemo(() => ({ favoriteId: categories.find((c) => c.slug === "favorite")?.id, vipId: categories.find((c) => c.slug === "vip")?.id, }), [categories]); const enriched = useMemo(() => items.map((a) => { const reversed = reverseName(a.name); const altParts = (a.altNames ?? "").split(/[,、,]/).map((s) => s.trim()).filter(Boolean); const haystack = [a.name, reversed ?? "", ...altParts].join(" ").toLowerCase(); return { actress: a, haystack, bucket: bucketFor(a.name) }; }), [items]); const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean); const searched = tokens.length === 0 ? enriched : enriched.filter(({ haystack }) => tokens.every((t) => haystack.includes(t))); const categoryFiltered = activeCategoryId == null ? searched : activeCategoryId === "unassigned" ? searched.filter(({ actress }) => actress.categories.length === 0) : searched.filter(({ actress }) => actress.categories.some((c) => c.id === activeCategoryId)); const unassignedCount = useMemo( () => searched.reduce((n, e) => (e.actress.categories.length === 0 ? n + 1 : n), 0), [searched], ); // Pill display order: ALL · VIP · Favorite · Not Assigned · everything // else. VIP and Favorite are seeded built-ins identified by slug; if // they're absent for any reason we just skip them. const orderedCategories = useMemo(() => { const vip = categories.find((c) => c.slug === "vip"); const fav = categories.find((c) => c.slug === "favorite"); const rest = categories.filter((c) => c.slug !== "favorite" && c.slug !== "vip"); return [...(vip ? [vip] : []), ...(fav ? [fav] : []), ...rest]; }, [categories]); const counts = useMemo(() => { const m: Record = {}; for (const e of categoryFiltered) m[e.bucket] = (m[e.bucket] ?? 0) + 1; return m; }, [categoryFiltered]); const categoryCounts = useMemo(() => { const m: Record = {}; for (const e of searched) { for (const c of e.actress.categories) m[c.id] = (m[c.id] ?? 0) + 1; } return m; }, [searched]); const visible = activeLetter ? categoryFiltered.filter((e) => e.bucket === activeLetter) : categoryFiltered; const grouped = useMemo(() => { const groups: Record = {}; for (const { actress, bucket } of visible) { (groups[bucket] ??= []).push(actress); } for (const k of Object.keys(groups)) { groups[k].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" })); } return groups; }, [visible]); const orderedBuckets = [...LETTERS, NON_LATIN].filter((b) => (grouped[b]?.length ?? 0) > 0); // Visible IDs in the exact rendered order — used for shift-click range and "select all visible". const orderedIds = useMemo(() => { const out: number[] = []; for (const b of orderedBuckets) for (const a of grouped[b]) out.push(a.id); return out; }, [orderedBuckets, grouped]); const allVisibleSelected = orderedIds.length > 0 && orderedIds.every((id) => sel.has(id)); const renderCategoryPill = (c: ActressCategory) => { const n = categoryCounts[c.id] ?? 0; const enabled = n > 0; const active = activeCategoryId === c.id; const color = c.color ?? "var(--color-cyan)"; return ( ); }; return (
setQuery(e.target.value)} placeholder="Filter Cast — Name, Reversed, Alt Names…" className="w-full glass rounded-lg pl-9 pr-9 py-2 text-sm outline-none focus:border-[var(--color-cyan)]" /> {query && ( )}
{categories.length > 0 && (
{orderedCategories .filter((c) => c.slug === "favorite" || c.slug === "vip") .map((c) => renderCategoryPill(c))} {orderedCategories .filter((c) => c.slug !== "favorite" && c.slug !== "vip") .map((c) => { return renderCategoryPill(c); })}
)}
{[...LETTERS, NON_LATIN].map((L) => { const n = counts[L] ?? 0; const enabled = n > 0; const active = activeLetter === L; return ( ); })}
{orderedBuckets.length === 0 && (
No matches.
)} {activeLetter === null ? (
{orderedBuckets.flatMap((b) => grouped[b]).map((a) => ( ))}
) : (
{orderedBuckets.map((b) => (

{b} {grouped[b].length}

{grouped[b].map((a) => ( ))}
))}
)}
); }