"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { Search, ChevronDown, Users, Building2, Film, Hash, FolderHeart, Tag, X, Check, Layers } from "lucide-react"; import { cn } from "@/lib/utils"; import { tabSupportsAnd, type FilterCriteria, type FilterTabKey } from "@/lib/filters"; export interface FilterOption { id: number; name: string; count?: number; } const TAB_META: Array<{ key: FilterTabKey; label: string; Icon: React.ComponentType<{ className?: string }> }> = [ { key: "actresses", label: "Actresses", Icon: Users }, { key: "studios", label: "Studios", Icon: Building2 }, { key: "series", label: "Series", Icon: Film }, { key: "categories", label: "Categories", Icon: Layers }, { key: "tags", label: "Tags", Icon: Tag }, { key: "genres", label: "Genres", Icon: Hash }, { key: "collections", label: "Collections", Icon: FolderHeart }, ]; export function MultiFilterPopover({ criteria, options, onChange, }: { criteria: FilterCriteria; options: Record; onChange: (next: FilterCriteria) => void; }) { const [open, setOpen] = useState(false); const [activeTab, setActiveTab] = useState("actresses"); const [search, setSearch] = useState(""); const wrapRef = useRef(null); useEffect(() => { if (!open) return; const onDoc = (e: MouseEvent) => { if (!wrapRef.current?.contains(e.target as Node)) setOpen(false); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, [open]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open]); const total = useMemo(() => { let n = 0; for (const t of TAB_META) n += criteria.ids[t.key].length; return n; }, [criteria]); function toggleId(tab: FilterTabKey, id: number) { const cur = criteria.ids[tab]; const next = cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id]; onChange({ ...criteria, ids: { ...criteria.ids, [tab]: next } }); } function setMode(tab: FilterTabKey, mode: "and" | "or") { onChange({ ...criteria, mode: { ...criteria.mode, [tab]: mode } }); } function clearTab(tab: FilterTabKey) { onChange({ ...criteria, ids: { ...criteria.ids, [tab]: [] } }); } function clearAllTabs() { onChange({ ...criteria, ids: { actresses: [], studios: [], series: [], genres: [], collections: [], tags: [], categories: [] }, }); } const tabOptions = options[activeTab] ?? []; const q = search.trim().toLowerCase(); const filteredOptions = q ? tabOptions.filter((o) => o.name.toLowerCase().includes(q)) : tabOptions; const supportsAnd = tabSupportsAnd(activeTab); return (
{open && (
e.stopPropagation()} >
{TAB_META.map(({ key, label, Icon }) => { const count = criteria.ids[key].length; const isActive = key === activeTab; return ( ); })}
setSearch(e.target.value)} placeholder={`Filter ${activeTab}…`} className="w-full glass rounded-lg pl-8 pr-2 py-1.5 text-sm outline-none focus:border-[var(--color-cyan)]" />
{supportsAnd && (
)}
{filteredOptions.length === 0 ? (
{q ? "No matches" : `No ${activeTab} yet`}
) : ( filteredOptions.map((o) => { const checked = criteria.ids[activeTab].includes(o.id); return ( ); }) )}
{supportsAnd ? "tap to toggle · AND = match all · OR = match any" : "tap to toggle"}
|
)}
); } export const FILTER_TABS = TAB_META;