/** * Renders the menu that switches the index page's feed between all dumps and a * category. * * @module */ import { type CSSProperties, useEffect, useLayoutEffect, useRef, useState, } from "react"; import { createPortal } from "react-dom"; import { useNavigate, useParams } from "react-router"; import { t } from "@lingui/core/macro"; import { useCategories } from "../../hooks/category/useCategories.ts"; import { DEFAULT_FEED_TAB } from "../../config/feedTabs.ts"; /** * Renders the category menu at the head of `FeedTabBar`. Picking a category * keeps the feed tab, from `/music/new` to `/cinema/new`, and "All" leads to * `/~/new`. It renders nothing while the site has no categories. */ export function CategorySwitcher(): React.JSX.Element | null { const { categories } = useCategories(); const navigate = useNavigate(); const { categorySlug, feedTab } = useParams(); const [open, setOpen] = useState(false); const [menuStyle, setMenuStyle] = useState({}); const wrapRef = useRef(null); const btnRef = useRef(null); const menuRef = useRef(null); // The menu is portaled out of the wrapper, so a click in it would count as // outside and close it before the option's click could navigate. useEffect(() => { if (!open) return; function onDown(e: MouseEvent) { const target = e.target as Node; if ( wrapRef.current && !wrapRef.current.contains(target) && menuRef.current && !menuRef.current.contains(target) ) { setOpen(false); } } function onKey(e: KeyboardEvent) { if (e.key === "Escape") setOpen(false); } document.addEventListener("mousedown", onDown); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); }; }, [open]); // The menu is fixed and portaled to , so the tab row's // `overflow-x: auto` can't clip it. It must follow the button on scroll. useLayoutEffect(() => { if (!open) return; const update = () => { const r = btnRef.current?.getBoundingClientRect(); if (r) setMenuStyle({ top: r.bottom + 6, left: r.left }); }; update(); globalThis.addEventListener("scroll", update, true); globalThis.addEventListener("resize", update); return () => { globalThis.removeEventListener("scroll", update, true); globalThis.removeEventListener("resize", update); }; }, [open]); if (categories.length === 0) return null; const value = categorySlug ?? "~"; const tab = feedTab ?? DEFAULT_FEED_TAB; const currentLabel = categorySlug ? categories.find((c) => c.slug === categorySlug)?.name ?? categorySlug : t`All`; const options = [ { value: "~", label: t`All` }, ...categories.map((c) => ({ value: c.slug, label: c.name })), ]; function select(slug: string) { setOpen(false); navigate(`/${slug}/${tab}`); } return (
{open && createPortal(
    {options.map((o) => (
  • ))}
, document.body, )}
); }