import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
import { CATEGORIES, PAGES, SHOP } from "../config/shop.ts";
import { checkoutUrl, count, load, server, subscribe, total } from "../lib/cart.ts";
import { useMediaQuery } from "../lib/hooks.ts";
import { collectionUrl, lineUrl, money } from "../lib/shop.ts";
import { get as getMode, server as modeServer, subscribe as onMode, toggle } from "../client/mode.ts";
import { Icon } from "./Icon.jsx";
const CART = "/cart/";
const HOVER = 180;
const LEAVE = 260;
const LIMIT = 12;
/* SEARCH */
let index = null;
async function hits() {
if (index) return index;
try {
index = await (await fetch("/search.json")).json();
} catch {
index = [];
}
return index;
}
function Finder({ links, on }) {
const [text, setText] = useState("");
const [found, setFound] = useState([]);
const box = useRef(null);
useEffect(() => {
if (on) box.current?.focus();
}, [on]);
useEffect(() => {
const want = text.trim().toLowerCase();
if (!want) return;
let live = true;
const words = want.split(/\s+/);
void hits().then((all) => {
if (!live) return;
setFound(all.filter((hit) => words.every((word) => `${hit.name} ${hit.kind} ${hit.tags ?? ""}`.toLowerCase().includes(word))).slice(0, LIMIT));
});
return () => {
live = false;
};
}, [text]);
const empty = !text.trim();
return (
);
}
/* BAG */
function Purse({ items, on }) {
const n = count(items);
return (
{n ? `Your Bag · ${n} item${n === 1 ? "" : "s"} · ${money(total(items))}` : "Your Bag"}
0}>
Your bag is empty.
Review Bag
{items.length ? (
Checkout
) : null}
);
}
/* HEADER */
export function Header({ route, catalog = [], fly = false }) {
const links = [{ slug: "all", name: "All", href: SHOP }, ...CATEGORIES.map(([slug, name]) => ({ slug, name, href: `${SHOP}${slug}/` })), { slug: "pages", name: "Pages", href: "/pages/" }];
const items = useSyncExternalStore(subscribe, load, server);
const mode = useSyncExternalStore(onMode, getMode, modeServer);
const wide = useMediaQuery("(min-width: 768px)");
const [open, setOpen] = useState("");
const [level, setLevel] = useState(1);
const timer = useRef(0);
const n = count(items);
const show = useCallback((name, deep = 1) => {
clearTimeout(timer.current);
setOpen(name);
setLevel(deep);
}, []);
const hide = useCallback(() => {
clearTimeout(timer.current);
setOpen("");
setLevel(1);
}, []);
const later = useCallback(
(name, wait) => {
clearTimeout(timer.current);
timer.current = setTimeout(() => (name ? show(name) : hide()), wait);
},
[hide, show],
);
useEffect(() => () => clearTimeout(timer.current), []);
useEffect(() => {
document.documentElement.classList.toggle("held", Boolean(open) && !wide);
}, [open, wide]);
useEffect(() => hide(), [hide, route, wide]);
useEffect(() => {
if (!open) return;
const key = (event) => {
if (event.key === "Escape") hide();
};
const away = (event) => {
if (event.target instanceof Element && !event.target.closest(".top")) hide();
};
document.addEventListener("keydown", key);
document.addEventListener("click", away);
return () => {
document.removeEventListener("keydown", key);
document.removeEventListener("click", away);
};
}, [hide, open]);
const sticky = open === "search" || open === "bag" || (open && !wide);
const current = (href) => (route === href || (href !== SHOP && route.startsWith(href)) ? "page" : undefined);
const rows = (slug) =>
slug === "all"
? CATEGORIES.map(([one, name]) => ({ title: name, href: `${SHOP}${one}/` }))
: slug === "pages"
? PAGES.filter((one) => one.href !== CART).map((one) => ({ title: one.name, href: one.href }))
: catalog.filter((row) => row.category === slug).map((row) => ({ title: row.title, href: collectionUrl(row.handle) }));
return (
);
}