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";
});