"use client"; import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { X } from "lucide-react"; import { cn } from "@/lib/utils"; import type { FilterCriteria, FilterTabKey, StatusAxisKey } from "@/lib/filters"; import { anyActive, EMPTY_STATUS } from "@/lib/filters"; import { FILTER_TABS, type FilterOption } from "./MultiFilterPopover"; import { useSelection } from "@/components/select/SelectionProvider"; const WATCHED_LABELS: Record = { watched: "Watched", unwatched: "Unwatched" }; const RATED_LABELS: Record = { rated: "Rated", unrated: "No Rating" }; const PRESENCE_LABELS: Record = { has: "Has", missing: "No" }; function pillFor(axis: StatusAxisKey, value: string): string | null { if (value === "all") return null; if (axis === "watched") return WATCHED_LABELS[value] ?? null; if (axis === "rated") return RATED_LABELS[value] ?? null; if (axis === "collection") return value === "has" ? "Has Collection" : "No Collection"; if (axis === "tags") return value === "has" ? "Has Tags" : "No Tags"; if (axis === "video") return value === "has" ? "Has Video" : "No Video"; return null; } const STATUS_AXES: StatusAxisKey[] = ["watched", "rated", "collection", "tags", "video"]; export function ActiveCriteriaStrip({ criteria, options, onChange, }: { criteria: FilterCriteria; options: Record; onChange: (next: FilterCriteria) => void; }) { // SSR has no document, so defer the portal until after mount. const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); const sel = useSelection(); const selectionActive = sel.ids.size > 0; if (!anyActive(criteria)) return null; if (!mounted) return null; const tabSections: Array<{ key: FilterTabKey; label: string; Icon: React.ComponentType<{ className?: string }>; pills: FilterOption[] }> = []; for (const t of FILTER_TABS) { const ids = criteria.ids[t.key]; if (ids.length === 0) continue; const optionMap = new Map(options[t.key].map((o) => [o.id, o])); const pills = ids.map((id) => optionMap.get(id)).filter((o): o is FilterOption => !!o); if (pills.length > 0) tabSections.push({ key: t.key, label: t.label, Icon: t.Icon, pills }); } function removeId(tab: FilterTabKey, id: number) { onChange({ ...criteria, ids: { ...criteria.ids, [tab]: criteria.ids[tab].filter((x) => x !== id) } }); } function resetAxis(key: StatusAxisKey) { onChange({ ...criteria, status: { ...criteria.status, [key]: "all" } as typeof criteria.status }); } function removeMark(m: typeof criteria.marks[number]) { onChange({ ...criteria, marks: criteria.marks.filter((x) => x !== m) }); } function clearAll() { onChange({ ids: { actresses: [], studios: [], series: [], genres: [], collections: [], tags: [], categories: [] }, mode: criteria.mode, status: { ...EMPTY_STATUS }, marks: [], }); } return createPortal(
Active Filters {tabSections.map((section, sIdx) => ( {section.pills.map((p, i) => ( removeId(section.key, p.id)} /> {i < section.pills.length - 1 && ( {criteria.mode[section.key].toUpperCase()} )} ))} {sIdx < tabSections.length - 1 && ( AND )} ))} {STATUS_AXES.map((axis) => { const label = pillFor(axis, criteria.status[axis]); if (!label) return null; return resetAxis(axis)} />; })} {criteria.marks.map((m) => ( removeMark(m)} /> ))}
, document.body, ); } function Pill({ kind, label, Icon, onRemove, }: { kind: "cyan" | "coral" | "amber" | "violet"; label: string; Icon?: React.ComponentType<{ className?: string }>; onRemove: () => void; }) { const cls = kind === "cyan" ? "bg-[var(--color-cyan)]/12 border-[var(--color-cyan)]/40 text-[var(--color-cyan)]" : kind === "amber" ? "border-amber-400/40 text-amber-200" : kind === "violet" ? "bg-[var(--color-violet)]/12 border-[var(--color-violet)]/40 text-[var(--color-violet)]" : "bg-[var(--color-coral)]/12 border-[var(--color-coral)]/40 text-[var(--color-coral)]"; return ( {Icon && } {label} ); }