/* ============================================================
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 `
`;
const initial = r.name.trim().charAt(0).toUpperCase();
return `
${esc(initial)}
`;
}
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 = ``; 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
${esc(r.city)} · ${STATUS_LABEL[r.type]}
${esc(r.name)}
${esc(r.desc)}
- Address${esc(r.address)}
${phone}${web}
- Good for
${tags || "—"}
`;
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();
});
})();