131 lines
4.0 KiB
JavaScript
131 lines
4.0 KiB
JavaScript
// 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 = `
|
||
|
|
<td>${escapeHtml(r.device_model)}</td>
|
||
|
|
<td>${escapeHtml(r.form_factor)}</td>
|
||
|
|
<td>${escapeHtml(r.cpu)}</td>
|
||
|
|
<td>${r.ram_gb}GB</td>
|
||
|
|
<td>${storageLabel(r.storage)}</td>
|
||
|
|
<td>${escapeHtml(r.wifi_chip)}</td>
|
||
|
|
<td><span class="badge ${r.status}">${r.status}</span></td>
|
||
|
|
<td>${escapeHtml(r.archy_version)}</td>
|
||
|
|
<td>${escapeHtml(r.tested_date)}</td>
|
||
|
|
`;
|
||
|
|
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(`<strong>Ethernet:</strong> ${escapeHtml(r.ethernet)}`);
|
||
|
|
parts.push(`<strong>Install method:</strong> ${escapeHtml(r.install_method)}`);
|
||
|
|
if (r.submitted_by) parts.push(`<strong>Submitted by:</strong> ${escapeHtml(r.submitted_by)}`);
|
||
|
|
if (r.issues) parts.push(`<strong>Issues:</strong>\n${escapeHtml(r.issues.trim())}`);
|
||
|
|
if (r.notes) parts.push(`<strong>Notes:</strong>\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;
|
||
|
|
});
|