"use client"; import { useEffect, useRef, useState } from "react"; import { Bookmark, ChevronDown, Gem, Star, MinusCircle, Package, Check, Eye, FolderHeart, Tag, Play } from "lucide-react"; import { cn } from "@/lib/utils"; import type { FilterCriteria, FilterStatus, MarkOption, StatusAxisKey } from "@/lib/filters"; import { totalStatusActive, EMPTY_STATUS } from "@/lib/filters"; const MARK_OPTIONS: Array<{ value: MarkOption; label: string; Icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>; tint: "cyan" | "amber" | "violet" | "muted"; }> = [ { value: "vip", label: "VIP", Icon: Gem, tint: "cyan" }, { value: "favorite", label: "Favorite", Icon: Star, tint: "amber" }, { value: "owned", label: "Owned", Icon: Package, tint: "violet" }, { value: "unmarked", label: "Unmarked", Icon: MinusCircle, tint: "muted" }, ]; type AxisOpt = { value: V; label: string }; type AxisConfig = { key: StatusAxisKey; label: string; Icon: React.ComponentType<{ className?: string }>; options: Array>; }; const WATCH_AXES: AxisConfig[] = [ { key: "watched", label: "Watched", Icon: Eye, options: [ { value: "all", label: "ALL" }, { value: "watched", label: "Watched" }, { value: "unwatched", label: "Unwatched" }, ]}, { key: "rated", label: "Rated", Icon: Star, options: [ { value: "all", label: "ALL" }, { value: "rated", label: "Rated" }, { value: "unrated", label: "No Rating" }, ]}, ]; const HAS_AXES: AxisConfig[] = [ { key: "collection", label: "Collection", Icon: FolderHeart, options: [ { value: "all", label: "ALL" }, { value: "has", label: "Has" }, { value: "missing", label: "Missing" }, ]}, { key: "tags", label: "Tags", Icon: Tag, options: [ { value: "all", label: "ALL" }, { value: "has", label: "Has" }, { value: "missing", label: "Missing" }, ]}, { key: "video", label: "Video", Icon: Play, options: [ { value: "all", label: "ALL" }, { value: "has", label: "Has" }, { value: "missing", label: "Missing" }, ]}, ]; export function MergedFilterPopover({ criteria, onChange, }: { criteria: FilterCriteria; onChange: (next: FilterCriteria) => void; }) { const [open, setOpen] = useState(false); const wrapRef = useRef(null); const markCount = criteria.marks.length; const stateCount = totalStatusActive(criteria); const total = markCount + stateCount; const active = total > 0; 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]); function toggleMark(value: MarkOption) { const has = criteria.marks.includes(value); const next = has ? criteria.marks.filter((m) => m !== value) : [...criteria.marks, value]; onChange({ ...criteria, marks: next }); } function setAxis(key: K, value: FilterStatus[K]) { onChange({ ...criteria, status: { ...criteria.status, [key]: value } }); } function resetAll() { onChange({ ...criteria, marks: [], status: { ...EMPTY_STATUS } }); } // Watch / Has section counts (used for the footer breakdown text only). const watchCount = (["watched", "rated"] as StatusAxisKey[]).filter((k) => criteria.status[k] !== "all").length; const hasCount = (["collection", "tags", "video"] as StatusAxisKey[]).filter((k) => criteria.status[k] !== "all").length; return (
{open && (
e.stopPropagation()} > {/* Section 1 — Marks (multi-select OR) */}
{MARK_OPTIONS.map(({ value, label, Icon, tint }) => { const on = criteria.marks.includes(value); return ( ); })}
{/* Section 2 — Watch State */} {/* Section 3 — Has… */} {/* Footer */}
{total === 0 ? "no filters set" : `${total} filter${total === 1 ? "" : "s"}` + (markCount > 0 ? ` · ${markCount} mark${markCount === 1 ? "" : "s"}` : "") + (watchCount > 0 ? ` · ${watchCount} watch` : "") + (hasCount > 0 ? ` · ${hasCount} has` : "")}
)}
); } function AxisSection({ axes, status, onSet, }: { axes: AxisConfig[]; status: FilterStatus; onSet: (key: K, value: FilterStatus[K]) => void; }) { return (
{axes.map(({ key, label, Icon, options }) => { const current = status[key]; return (
{label}
{options.map((o) => { const on = current === o.value; return ( ); })}
); })}
); }