// No framework, no build step — this is a plain JSON file (site/data.json, // generated by scripts/build.py) rendered client-side. Keeps the "add a // device" contribution path as simple as adding one YAML file. let reports = []; let sortKey = "device_model"; let sortDir = 1; const rowsEl = document.getElementById("rows"); const searchEl = document.getElementById("search"); const statusEl = document.getElementById("status-filter"); const formFactorEl = document.getElementById("form-factor-filter"); const countEl = document.getElementById("count"); const emptyEl = document.getElementById("empty"); function storageLabel(storage) { return `${storage.size_gb}GB ${storage.type.toUpperCase()}`; } function matchesFilters(r, query, status, formFactor) { if (status && r.status !== status) return false; if (formFactor && r.form_factor !== formFactor) return false; if (!query) return true; const haystack = [r.device_model, r.cpu, r.wifi_chip, r.ethernet] .filter(Boolean) .join(" ") .toLowerCase(); return haystack.includes(query); } function sortValue(r, key) { if (key === "storage") return r.storage.size_gb; return r[key]; } function render() { const query = searchEl.value.trim().toLowerCase(); const status = statusEl.value; const formFactor = formFactorEl.value; const filtered = reports.filter((r) => matchesFilters(r, query, status, formFactor)); filtered.sort((a, b) => { const av = sortValue(a, sortKey); const bv = sortValue(b, sortKey); if (av < bv) return -1 * sortDir; if (av > bv) return 1 * sortDir; return 0; }); rowsEl.innerHTML = ""; for (const r of filtered) { const tr = document.createElement("tr"); tr.className = "expandable"; tr.innerHTML = ` ${escapeHtml(r.device_model)} ${escapeHtml(r.form_factor)} ${escapeHtml(r.cpu)} ${r.ram_gb}GB ${storageLabel(r.storage)} ${escapeHtml(r.wifi_chip)} ${r.status} ${escapeHtml(r.archy_version)} ${escapeHtml(r.tested_date)} `; tr.addEventListener("click", () => toggleDetail(tr, r)); rowsEl.appendChild(tr); } countEl.textContent = `${filtered.length} of ${reports.length} report${reports.length === 1 ? "" : "s"}`; emptyEl.hidden = filtered.length !== 0; } function toggleDetail(tr, r) { const next = tr.nextElementSibling; if (next && next.classList.contains("detail-row")) { next.remove(); return; } document.querySelectorAll(".detail-row").forEach((el) => el.remove()); const detail = document.createElement("tr"); detail.className = "detail-row"; const parts = []; if (r.ethernet) parts.push(`Ethernet: ${escapeHtml(r.ethernet)}`); parts.push(`Install method: ${escapeHtml(r.install_method)}`); if (r.submitted_by) parts.push(`Submitted by: ${escapeHtml(r.submitted_by)}`); if (r.issues) parts.push(`Issues:\n${escapeHtml(r.issues.trim())}`); if (r.notes) parts.push(`Notes:\n${escapeHtml(r.notes.trim())}`); const td = document.createElement("td"); td.colSpan = 9; td.innerHTML = parts.join("\n\n"); detail.appendChild(td); tr.after(detail); } function escapeHtml(s) { const div = document.createElement("div"); div.textContent = s ?? ""; return div.innerHTML; } document.querySelectorAll("th[data-sort]").forEach((th) => { th.addEventListener("click", () => { const key = th.dataset.sort; if (sortKey === key) { sortDir *= -1; } else { sortKey = key; sortDir = 1; } render(); }); }); searchEl.addEventListener("input", render); statusEl.addEventListener("change", render); formFactorEl.addEventListener("change", render); fetch("data.json") .then((res) => res.json()) .then((data) => { reports = data; render(); }) .catch((err) => { emptyEl.hidden = false; emptyEl.textContent = "Failed to load data.json — " + err; });