/* ============================================================ MNveg — app.js (rendering, filtering, maps, guides) You shouldn't need to edit this file to add restaurants — that all happens in js/data.js. ============================================================ */ (function () { "use strict"; const STATUS_LABEL = { "vegan": "Vegan", "vegetarian": "Vegetarian", "vegan-friendly": "Veg-Friendly" }; const ALL_TAGS = ["breakfast", "alcohol", "delivery", "takeout", "dessert"]; const byCity = (a, b) => a.city.localeCompare(b.city) || a.name.localeCompare(b.name); const esc = (s) => String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); /* ---------- mobile nav ---------- */ const toggle = document.querySelector(".nav-toggle"); if (toggle) { toggle.addEventListener("click", () => { document.querySelector(".nav-links").classList.toggle("open"); }); } /* ---------- card markup ---------- */ function mediaHTML(r) { if (r.image) return `${esc(r.name)}`; const initial = r.name.trim().charAt(0).toUpperCase(); return ``; } function cardHTML(r) { const tags = (r.tags || []).map(t => `${esc(t)}`).join(""); return `
${STATUS_LABEL[r.type] || r.type} ${mediaHTML(r)}
${esc(r.city)}

${esc(r.name)}

${esc(r.desc)}

${tags}
`; } /* ============================================================ HOME PAGE — filters + grid ============================================================ */ function initHome() { const grid = document.getElementById("card-grid"); if (!grid) return; const data = window.RESTAURANTS || []; // build city dropdown const citySel = document.getElementById("city-select"); const cities = [...new Set(data.map(r => r.city))].sort(); cities.forEach(c => { const o = document.createElement("option"); o.value = c; o.textContent = c; citySel.appendChild(o); }); const state = { status: new Set(), tags: new Set(), city: "", q: "" }; function matches(r) { if (state.status.size && !state.status.has(r.type)) return false; if (state.tags.size) { for (const t of state.tags) if (!(r.tags || []).includes(t)) return false; // AND } if (state.city && r.city !== state.city) return false; if (state.q) { const hay = (r.name + " " + r.desc + " " + r.city).toLowerCase(); if (!hay.includes(state.q)) return false; } return true; } function render() { const list = data.filter(matches).sort(byCity); grid.innerHTML = list.length ? list.map(cardHTML).join("") : `

No spots match those filters.

Try removing one, or clear all to see everything.

`; const count = document.getElementById("result-count"); if (count) count.textContent = `${list.length} ${list.length === 1 ? "place" : "places"}`; } // chip groups document.querySelectorAll("[data-status]").forEach(chip => { chip.addEventListener("click", () => { const v = chip.dataset.status; chip.getAttribute("aria-pressed") === "true" ? (state.status.delete(v), chip.setAttribute("aria-pressed", "false")) : (state.status.add(v), chip.setAttribute("aria-pressed", "true")); render(); }); }); document.querySelectorAll("[data-tag]").forEach(chip => { chip.addEventListener("click", () => { const v = chip.dataset.tag; chip.getAttribute("aria-pressed") === "true" ? (state.tags.delete(v), chip.setAttribute("aria-pressed", "false")) : (state.tags.add(v), chip.setAttribute("aria-pressed", "true")); render(); }); }); citySel.addEventListener("change", () => { state.city = citySel.value; render(); }); const search = document.getElementById("search"); search.addEventListener("input", () => { state.q = search.value.trim().toLowerCase(); render(); }); document.getElementById("clear-filters").addEventListener("click", () => { state.status.clear(); state.tags.clear(); state.city = ""; state.q = ""; document.querySelectorAll('[aria-pressed="true"]').forEach(c => c.setAttribute("aria-pressed", "false")); citySel.value = ""; search.value = ""; render(); }); render(); } /* ============================================================ DETAIL PAGE ============================================================ */ function initDetail() { const root = document.getElementById("detail"); if (!root) return; const slug = new URLSearchParams(location.search).get("slug"); const r = (window.RESTAURANTS || []).find(x => x.slug === slug); if (!r) { root.innerHTML = `

Restaurant not found.

← Back to the directory

`; return; } document.title = `${r.name} — MNveg`; const tags = (r.tags || []).map(t => `${esc(t)}`).join(""); const phone = r.phone ? `
  • Phone${esc(r.phone)}
  • ` : ""; const web = r.website ? `
  • Website${esc(r.website)}
  • ` : ""; root.innerHTML = ` ← All restaurants
    ${STATUS_LABEL[r.type]}${r.image ? `${esc(r.name)}` : `
    ${esc(r.name.charAt(0))}
    `}
    ${esc(r.city)} · ${STATUS_LABEL[r.type]}

    ${esc(r.name)}

    ${esc(r.desc)}

    `; if (window.L && typeof r.lat === "number") { const map = L.map("detail-map", { scrollWheelZoom: false }).setView([r.lat, r.lng], 15); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap", maxZoom: 19 }).addTo(map); L.marker([r.lat, r.lng]).addTo(map).bindPopup(`${esc(r.name)}`); } } /* ============================================================ MAP PAGE ============================================================ */ function initMap() { const el = document.getElementById("map"); if (!el || !window.L) return; const data = (window.RESTAURANTS || []).filter(r => typeof r.lat === "number"); const map = L.map("map").setView([46.0, -93.6], 6); // centered on Minnesota L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap", maxZoom: 19 }).addTo(map); const colors = { "vegan": "#143a2b", "vegetarian": "#9a6a1a", "vegan-friendly": "#7d8a86" }; const bounds = []; data.forEach(r => { bounds.push([r.lat, r.lng]); const dot = L.divIcon({ className: "", html: `
    `, iconSize: [16, 16], iconAnchor: [8, 8] }); L.marker([r.lat, r.lng], { icon: dot }).addTo(map).bindPopup( ` ${esc(r.name)}
    ${esc(r.city)}
    View details →` ); }); if (bounds.length) map.fitBounds(bounds, { padding: [50, 50] }); } /* ============================================================ GUIDES ============================================================ */ function initGuides() { const list = document.getElementById("guide-list"); if (!list) return; const guides = (window.GUIDES || []).slice().sort((a, b) => b.date.localeCompare(a.date)); list.innerHTML = guides.map(g => ` ${new Date(g.date).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}

    ${esc(g.title)}

    ${esc(g.excerpt)}

    `).join(""); } function initGuide() { const root = document.getElementById("article"); if (!root) return; const slug = new URLSearchParams(location.search).get("slug"); const g = (window.GUIDES || []).find(x => x.slug === slug); if (!g) { root.innerHTML = `

    Guide not found

    ← All guides

    `; return; } document.title = `${g.title} — MNveg`; const paras = g.body.split(/\n\s*\n/).map(p => `

    ${esc(p)}

    `).join(""); root.innerHTML = ` ← All guides ${new Date(g.date).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}

    ${esc(g.title)}

    ${paras}`; } document.addEventListener("DOMContentLoaded", () => { initHome(); initDetail(); initMap(); initGuides(); initGuide(); }); })();