/* Bar Pivoine — main.js */ // ── Taxonomy slug helper (matches design's taxSlug) ────────────────────────── function taxSlug(s) { return String(s).toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } // ── Build taxonomy facets from cocktail list ───────────────────────────────── function buildTax(list) { const acc = { alcoholic: {}, category: {}, glass: {}, ingredient: {} }; const add = (bucket, raw) => { if (!raw) return; const slug = taxSlug(raw); if (!slug) return; const e = bucket[slug] || (bucket[slug] = { value: raw, count: 0, slug }); e.count++; }; list.forEach(c => { add(acc.alcoholic, c.alcoholic); add(acc.category, c.category); add(acc.glass, c.glass); (c.ingredients || []).forEach(i => add(acc.ingredient, i.name)); }); const sort = o => Object.values(o).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)); return { alcoholic: sort(acc.alcoholic), category: sort(acc.category), glass: sort(acc.glass), ingredient: sort(acc.ingredient), }; } // ── HTMX progress bar ──────────────────────────────────────────────────────── (function () { const bar = document.getElementById("progress-bar"); if (!bar) return; document.addEventListener("htmx:beforeRequest", () => bar.classList.add("htmx-request")); document.addEventListener("htmx:afterSwap", () => { bar.classList.remove("htmx-request"); }); document.addEventListener("htmx:responseError", () => bar.classList.remove("htmx-request")); })(); // ── Alpine store ───────────────────────────────────────────────────────────── document.addEventListener("alpine:init", () => { Alpine.store("nav", { open: false }); }); // ── Cocktail search + filter component (Alpine x-data) ─────────────────────── function cocktailSearch() { return { q: "", active: { alcoholic: "", category: "", glass: "", ingredient: "" }, page: 1, perPage: 24, openFilter: null, all: [], tax: { alcoholic: [], category: [], glass: [], ingredient: [] }, init() { this.all = window.__COCKTAILS__ || []; this.tax = buildTax(this.all); const p = new URLSearchParams(window.location.search); this.q = p.get("q") || ""; this.active.alcoholic = p.get("alcoholic") || ""; this.active.category = p.get("category") || ""; this.active.glass = p.get("glass") || ""; this.active.ingredient = p.get("ingredient") || ""; this.page = parseInt(p.get("page") || "1", 10); // Close any open filter on outside click document.addEventListener("click", e => { if (!e.target.closest("[data-fgroup]")) this.openFilter = null; }); }, get filtered() { const needle = this.q.trim().toLowerCase(); return this.all.filter(c => { if (this.active.alcoholic && taxSlug(c.alcoholic) !== this.active.alcoholic) return false; if (this.active.category && taxSlug(c.category) !== this.active.category) return false; if (this.active.glass && taxSlug(c.glass) !== this.active.glass) return false; if (this.active.ingredient && !c.ingredients.some(i => taxSlug(i.name) === this.active.ingredient)) return false; if (needle) { const hay = (c.name + " " + c.category + " " + c.glass + " " + c.ingredients.map(i => i.name).join(" ")).toLowerCase(); if (!hay.includes(needle)) return false; } return true; }); }, get totalPages() { return Math.max(1, Math.ceil(this.filtered.length / this.perPage)); }, get paged() { const s = (this.page - 1) * this.perPage; return this.filtered.slice(s, s + this.perPage); }, get activeCount() { return Object.values(this.active).filter(Boolean).length + (this.q ? 1 : 0); }, get isFiltered() { return this.activeCount > 0; }, nobLabel(c) { if (c.alcoholic === "Alcoholic") return "Spirited"; if (c.alcoholic === "Non alcoholic") return "Zero-proof"; return "Optional"; }, valLabel(key, slug) { const t = this.tax[key].find(i => i.slug === slug); return t ? t.value : slug; }, toggleFilter(key) { this.openFilter = this.openFilter === key ? null : key; }, toggle(key, slug) { this.active[key] = this.active[key] === slug ? "" : slug; this.page = 1; this.openFilter = null; this.pushState(); }, setQuery(val) { this.q = val; this.page = 1; this.pushState(); }, clearAll() { this.q = ""; this.active = { alcoholic: "", category: "", glass: "", ingredient: "" }; this.page = 1; this.pushState(); }, changePage(n) { this.page = Math.max(1, Math.min(this.totalPages, n)); this.pushState(); window.scrollTo({ top: 0, behavior: "smooth" }); }, pagerItems() { const total = this.totalPages, page = this.page; const clamp = n => Math.max(1, Math.min(total, n)); const show = new Set([1, 2, clamp(page-2), clamp(page-1), page, clamp(page+1), clamp(page+2), total-1, total] .filter(n => n >= 1 && n <= total)); const sorted = [...show].sort((a, b) => a - b); const items = []; for (let i = 0; i < sorted.length; i++) { if (i > 0 && sorted[i] - sorted[i-1] > 1) items.push({ type: "dot" }); items.push({ type: "page", n: sorted[i] }); } return items; }, pushState() { const p = new URLSearchParams(); if (this.q) p.set("q", this.q); if (this.active.alcoholic) p.set("alcoholic", this.active.alcoholic); if (this.active.category) p.set("category", this.active.category); if (this.active.glass) p.set("glass", this.active.glass); if (this.active.ingredient) p.set("ingredient", this.active.ingredient); if (this.page > 1) p.set("page", this.page); history.replaceState({}, "", p.toString() ? "?" + p : window.location.pathname); }, }; } window.cocktailSearch = cocktailSearch; window.taxSlug = taxSlug; // ── Page transition on HTMX swap ───────────────────────────────────────────── document.addEventListener("htmx:afterSwap", () => { const main = document.getElementById("main-content"); if (!main) return; main.style.animation = "none"; void main.offsetHeight; main.style.animation = "page-in 300ms ease-out"; }); document.addEventListener("htmx:afterSettle", () => { window.scrollTo({ top: 0, behavior: "instant" }); });