TheBird

Shop.jsx

4.4 kB · jsx · 113 lines

1import { Breadcrumbs } from "../components/Breadcrumbs.jsx";2import { Chips } from "../components/Chips.jsx";3import { ProductGrid } from "../components/ProductGrid.jsx";4import { bump, useSearch } from "../lib/hooks.ts";56const SORTS = [7  ["newest", "Newest"],8  ["expiring", "Expiring soon"],9  ["low", "Price low to high"],10  ["high", "Price high to low"],11];1213const KEYS = ["design", "group", "secondary"];1415const FIRST = SORTS[0][0];1617const RANK = {18  expiring: (a, b) => (a.created ?? "").localeCompare(b.created ?? ""),19  low: (a, b) => Number(a.price) - Number(b.price),20  high: (a, b) => Number(b.price) - Number(a.price),21};2223const low = (text) => String(text ?? "").trim().toLowerCase();2425const words = (product, key) => (key === "secondary" ? product.secondary ?? [] : [product[key] ?? ""]).map(low).filter(Boolean);2627function state(search) {28  const params = new URLSearchParams(search);29  const picked = {};30  for (const key of KEYS) picked[key] = new Set((params.get(key) ?? "").split(",").map(low).filter(Boolean));31  const want = params.get("sort") ?? "";32  return { picked, sort: SORTS.some(([value]) => value === want) ? want : FIRST };33}3435function write(picked, sort) {36  const params = new URLSearchParams(location.search);37  for (const key of KEYS) {38    const list = [...picked[key]];39    if (list.length) params.set(key, list.join(","));40    else params.delete(key);41  }42  if (sort === FIRST) params.delete("sort");43  else params.set("sort", sort);44  const query = params.toString();45  history.replaceState(history.state, "", `${location.pathname}${query ? `?${query}` : ""}${location.hash}`);46  bump();47}4849function Facets({ facets, picked, sort, shown, all }) {50  const active = KEYS.some((key) => picked[key].size) || sort !== FIRST;51  const toggle = (key, value) => {52    const next = new Set(picked[key]);53    if (next.has(value)) next.delete(value);54    else next.add(value);55    write({ ...picked, [key]: next }, sort);56  };57  return (58    <div className="facets">59      {facets.map((facet) => (60        <div key={facet.key} className="facet">61          <span className="fine">{facet.label}</span>62          {facet.values.map((value) => (63            <button key={value.name} type="button" className={facet.key === "design" ? "mono" : undefined} aria-pressed={picked[facet.key].has(low(value.name)) ? "true" : "false"} onClick={() => toggle(facet.key, low(value.name))}>64              <span>{value.name}</span>65              <span className="n">{value.n}</span>66            </button>67          ))}68        </div>69      ))}70      <div className="facet">71        <label className="sort">72          <span className="fine">Sort</span>73          <select value={sort} onChange={(event) => write(picked, event.target.value)}>74            {SORTS.map(([value, name]) => (75              <option key={value} value={value}>76                {name}77              </option>78            ))}79          </select>80        </label>81        <span className="fine">{active ? `${shown} of ${all}` : ""}</span>82        {active ? (83          <button type="button" onClick={() => write({ design: new Set(), group: new Set(), secondary: new Set() }, FIRST)}>84            Clear85          </button>86        ) : null}87      </div>88    </div>89  );90}9192export function Shop({ trail, title, lead, chips, current, products, facets = [], tiles = false, now }) {93  const { picked, sort } = state(useSearch());94  const keep = (product) => KEYS.every((key) => !picked[key].size || words(product, key).some((value) => picked[key].has(value)));95  const order = new Map(products.map((product, i) => [product.key, i]));96  const list = RANK[sort] ? [...products].sort((a, b) => RANK[sort](a, b) || order.get(a.key) - order.get(b.key)) : products;97  const shown = list.filter(keep).length;98  return (99    <div className="wrap">100      <Breadcrumbs trail={trail} />101      <div className="page-head">102        <h1>{title}</h1>103        {lead ? <p className="lead">{lead}</p> : null}104      </div>105      {chips.map((group) => (106        <Chips key={group.label} label={group.label} cards={group.cards} current={current} />107      ))}108      {products.length > 1 ? <Facets facets={facets} picked={picked} sort={sort} shown={shown} all={products.length} /> : null}109      <ProductGrid products={list} eager={4} tiles={tiles} hide={(product) => !keep(product)} now={now} />110      {products.length > 1 && !shown ? <p className="lead empty">Nothing matches. Clear a filter.</p> : null}111    </div>112  );113}