| 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 = ` |
| <div class="stat-value">${card.value}</div> |
| <div class="stat-label">${card.label}</div> |
| `; |
| 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 = `<div class="empty">Failed to load registry data: ${error.message}</div>`; |
| resultCount.textContent = "Error"; |
| }); |
|
|