"use client"; /** * SearchFiltersMenu — collapses the search-page filter chip row into * a single trigger + multi-select dropdown. * * Trigger button: * • Shows "Filters" + a count badge when one or more filters are * active. The badge sums Users (1, if on) OR content-type count * (Users is mutually exclusive with content narrowing). * * Dropdown content: * • "Mode" section — single Users toggle (drives the @ prefix on * the input via onToggleUsers). * • "Content types" section — Books / Letters / Transcriptions / … * each a checkbox. When Users is on, this group is disabled * (content narrowing doesn't apply to user search). * * Layout: popover anchored to the trigger on desktop; bottom drawer on * mobile — same modal-vs-drawer convention the rest of the app's menus * use (PostCardMenu, SharePostSheet, CollectionsDrawer). * * "All" is intentionally NOT a checkbox — it's the implicit state when * zero content types are selected. We render no row for it. */ import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { AtSign, Check, ChevronDown, SlidersHorizontal, } from "lucide-react"; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, } from "@/components/ui/drawer"; import { cn } from "@/lib/utils"; export type ContentTab = { readonly label: string; readonly value: string }; export function SearchFiltersMenu({ contentTabs, selectedTypes, onTypeChange, isUserChipActive, onToggleUsers, isMobile, }: { contentTabs: readonly ContentTab[]; selectedTypes: Set; onTypeChange: (value: string) => void; isUserChipActive: boolean; onToggleUsers: () => void; isMobile: boolean; }) { const [open, setOpen] = useState(false); const triggerRef = useRef(null); const popoverRef = useRef(null); const [pos, setPos] = useState<{ top: number; left: number } | null>(null); // Users mode is mutually exclusive with content narrowing, so the // active count is one OR the other — never their sum. const activeCount = isUserChipActive ? 1 : selectedTypes.size; // Desktop popover positioning — below the trigger, left-aligned. useEffect(() => { if (!open || isMobile) { setPos(null); return; } function recompute() { const btn = triggerRef.current; if (!btn) return; const rect = btn.getBoundingClientRect(); const width = 280; const pad = 8; let left = rect.left; if (left + width > window.innerWidth - pad) { left = window.innerWidth - width - pad; } if (left < pad) left = pad; const top = rect.bottom + 6; setPos({ top, left }); } recompute(); window.addEventListener("resize", recompute); window.addEventListener("scroll", recompute, true); return () => { window.removeEventListener("resize", recompute); window.removeEventListener("scroll", recompute, true); }; }, [open, isMobile]); // Click-outside + Escape close (desktop only; mobile uses Drawer's // own dismiss handlers). useEffect(() => { if (!open || isMobile) return; function onPointer(e: PointerEvent) { if (popoverRef.current?.contains(e.target as Node)) return; if (triggerRef.current?.contains(e.target as Node)) return; setOpen(false); } function onKey(e: KeyboardEvent) { if (e.key === "Escape") setOpen(false); } const t = setTimeout(() => { document.addEventListener("pointerdown", onPointer); document.addEventListener("keydown", onKey); }, 0); return () => { clearTimeout(t); document.removeEventListener("pointerdown", onPointer); document.removeEventListener("keydown", onKey); }; }, [open, isMobile]); const trigger = ( ); const body = (
} checked={isUserChipActive} onChange={onToggleUsers} />
{contentTabs // "All" is the implicit default state — no row needed. .filter((t) => t.value !== "all") .map((tab) => { const checked = !isUserChipActive && selectedTypes.has(tab.value); return ( onTypeChange(tab.value)} /> ); })}
); // Mobile: bottom drawer if (isMobile) { return ( <> {trigger} Filters {body} ); } // Desktop: portaled popover return ( <> {trigger} {open && pos ? createPortal(
{body}
, document.body, ) : null} ); } // ─── Pieces ──────────────────────────────────────────────────────────── function FilterSection({ label, children, }: { label: string; children: React.ReactNode; }) { return (

{label}

{children}
); } function FilterRow({ label, icon, checked, disabled = false, onChange, }: { label: string; icon?: React.ReactNode; checked: boolean; disabled?: boolean; onChange: () => void; }) { return ( ); }