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}