const dataUrl = "./data/registry.json"; const searchInput = document.getElementById("search-input"); const sphereFilter = document.getElementById("sphere-filter"); const comboFilter = document.getElementById("combo-filter"); const statusFilter = document.getElementById("status-filter"); const artifactFilter = document.getElementById("artifact-filter"); const registryGrid = document.getElementById("registry-grid"); const stats = document.getElementById("stats"); const resultCount = document.getElementById("result-count"); const cardTemplate = document.getElementById("card-template"); let registryEntries = []; function titleCase(value) { return value .split(/[\s_-]+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function prettyLabel(key, value) { if (!value) { return ""; } if (key === "sphere") { return titleCase(value); } if (key === "artifactType" || key === "validationStage") { return titleCase(value); } return value; } function uniqueValues(entries, key) { return [...new Set(entries.map((entry) => entry[key]).filter(Boolean))].sort(); } function fillSelect(select, values, key) { for (const value of values) { const option = document.createElement("option"); option.value = value; option.textContent = prettyLabel(key, value); select.appendChild(option); } } function buildStats(entries) { const bySphere = ["science", "entrepreneurship", "technology"].map((sphere) => ({ label: prettyLabel("sphere", sphere), value: entries.filter((entry) => entry.primarySphere === sphere).length, className: sphere, })); const cards = [ { label: "Total entries", value: entries.length, className: "total" }, ...bySphere, { label: "Hybrid lanes", value: entries.filter((entry) => entry.combo.includes("+")).length, className: "hybrid", }, ]; stats.innerHTML = ""; for (const card of cards) { const wrapper = document.createElement("article"); wrapper.className = `stat ${card.className}`; wrapper.innerHTML = `
${card.value}
${card.label}
`; stats.appendChild(wrapper); } } function setText(target, value) { target.textContent = value && value.length ? value : "—"; } function renderCards(entries) { registryGrid.innerHTML = ""; resultCount.textContent = `${entries.length} result${entries.length === 1 ? "" : "s"}`; if (!entries.length) { const empty = document.createElement("div"); empty.className = "empty"; empty.textContent = "No entries match the current filters yet."; registryGrid.appendChild(empty); return; } for (const entry of entries) { const fragment = cardTemplate.content.cloneNode(true); const sphereBadge = fragment.querySelector(".badge.sphere"); const comboBadge = fragment.querySelector(".badge.combo"); const statusBadge = fragment.querySelector(".badge.status"); const title = fragment.querySelector(".title"); const summary = fragment.querySelector(".summary"); const track = fragment.querySelector(".track"); const artifactType = fragment.querySelector(".artifact-type"); const secondarySpheres = fragment.querySelector(".secondary-spheres"); const deliveryLayers = fragment.querySelector(".delivery-layers"); const validationStage = fragment.querySelector(".validation-stage"); const tags = fragment.querySelector(".tags"); const links = fragment.querySelector(".links"); sphereBadge.textContent = prettyLabel("sphere", entry.primarySphere); sphereBadge.classList.add(entry.primarySphere); comboBadge.textContent = entry.combo; comboBadge.classList.add(`combo-${entry.combo.toLowerCase().replace(/\+/g, "-")}`); statusBadge.textContent = prettyLabel("status", entry.status); title.textContent = entry.title; summary.textContent = entry.summary; setText(track, entry.track); setText(artifactType, prettyLabel("artifactType", entry.artifactType)); setText( secondarySpheres, entry.secondarySpheres.length ? entry.secondarySpheres.map((item) => prettyLabel("sphere", item)).join(", ") : "", ); setText(deliveryLayers, entry.deliveryLayers.join(", ")); setText(validationStage, prettyLabel("validationStage", entry.validationStage)); setText(tags, entry.tags.join(", ")); for (const link of entry.links) { const anchor = document.createElement("a"); anchor.href = link.href; anchor.textContent = link.label; anchor.target = "_blank"; anchor.rel = "noreferrer"; links.appendChild(anchor); } registryGrid.appendChild(fragment); } } function applyFilters() { const query = searchInput.value.trim().toLowerCase(); const sphere = sphereFilter.value; const combo = comboFilter.value; const status = statusFilter.value; const artifact = artifactFilter.value; const filtered = registryEntries.filter((entry) => { const matchesSphere = sphere === "all" || entry.primarySphere === sphere; const matchesCombo = combo === "all" || entry.combo === combo; const matchesStatus = status === "all" || entry.status === status; const matchesArtifact = artifact === "all" || entry.artifactType === artifact; const haystack = [ entry.title, entry.summary, entry.track, entry.entryType, entry.primarySphere, entry.combo, entry.artifactType, entry.validationStage, ...entry.secondarySpheres, ...entry.deliveryLayers, ...entry.tags, ] .join(" ") .toLowerCase(); const matchesQuery = !query || haystack.includes(query); return matchesSphere && matchesCombo && matchesStatus && matchesArtifact && matchesQuery; }); renderCards(filtered); } async function init() { const response = await fetch(dataUrl); registryEntries = await response.json(); fillSelect(sphereFilter, uniqueValues(registryEntries, "primarySphere"), "sphere"); fillSelect(comboFilter, uniqueValues(registryEntries, "combo"), "combo"); fillSelect(statusFilter, uniqueValues(registryEntries, "status"), "status"); fillSelect(artifactFilter, uniqueValues(registryEntries, "artifactType"), "artifactType"); buildStats(registryEntries); renderCards(registryEntries); searchInput.addEventListener("input", applyFilters); sphereFilter.addEventListener("change", applyFilters); comboFilter.addEventListener("change", applyFilters); statusFilter.addEventListener("change", applyFilters); artifactFilter.addEventListener("change", applyFilters); } init().catch((error) => { registryGrid.innerHTML = `
Failed to load registry data: ${error.message}
`; resultCount.textContent = "Error"; });