14e8087aac
- Implemented a new web interface for previewing designs, allowing users to search and filter by category. - Created `app.js` to handle state management, event binding, and rendering of design records. - Developed `index.html` as the main structure for the design gallery, including search input and design list. - Added `styles.css` for styling the design gallery and ensuring a responsive layout. - Updated Rust backend to support site generation, including new commands for building and serving the site. - Refactored catalog parsing to support new data structure for design entries. - Enhanced the sync process to collect site records and generate appropriate URLs for previews. - Introduced a new serving mechanism for the generated site, allowing local previews of designs.
204 lines
6.6 KiB
JavaScript
204 lines
6.6 KiB
JavaScript
const state = {
|
|
records: [],
|
|
query: "",
|
|
category: "All",
|
|
mode: "light",
|
|
selected: null,
|
|
};
|
|
|
|
const refs = {
|
|
search: document.getElementById("search"),
|
|
chips: document.getElementById("chips"),
|
|
list: document.getElementById("design-list"),
|
|
heroTitle: document.getElementById("hero-title"),
|
|
heroSub: document.getElementById("hero-sub"),
|
|
heroBrief: document.getElementById("hero-brief"),
|
|
previewFrame: document.getElementById("preview-frame"),
|
|
previewLabel: document.getElementById("preview-label"),
|
|
previewPath: document.getElementById("preview-path"),
|
|
actionLight: document.getElementById("action-light"),
|
|
actionDark: document.getElementById("action-dark"),
|
|
actionMd: document.getElementById("action-md"),
|
|
actionSource: document.getElementById("action-source"),
|
|
actionStatus: document.getElementById("action-status"),
|
|
};
|
|
|
|
async function bootstrap() {
|
|
try {
|
|
const response = await fetch("./data/designs.json", { cache: "no-store" });
|
|
state.records = await response.json();
|
|
state.selected = state.records[0]?.slug ?? null;
|
|
bindEvents();
|
|
render();
|
|
} catch (error) {
|
|
console.error(error);
|
|
refs.list.innerHTML = '<div class="empty-state">Failed to load local design data.</div>';
|
|
refs.heroTitle.textContent = "Load failed";
|
|
refs.heroSub.textContent = "Check site/data/designs.json";
|
|
}
|
|
}
|
|
|
|
function bindEvents() {
|
|
refs.search.addEventListener("input", event => {
|
|
state.query = event.target.value.trim().toLowerCase();
|
|
render();
|
|
});
|
|
|
|
refs.chips.addEventListener("click", event => {
|
|
const button = event.target.closest("[data-category]");
|
|
if (!button) return;
|
|
state.category = button.dataset.category;
|
|
render();
|
|
});
|
|
|
|
refs.list.addEventListener("click", event => {
|
|
const card = event.target.closest("[data-slug]");
|
|
if (!card) return;
|
|
state.selected = card.dataset.slug;
|
|
render();
|
|
});
|
|
|
|
refs.actionLight.addEventListener("click", () => {
|
|
state.mode = "light";
|
|
render();
|
|
});
|
|
|
|
refs.actionDark.addEventListener("click", () => {
|
|
state.mode = "dark";
|
|
render();
|
|
});
|
|
}
|
|
|
|
function categories() {
|
|
return ["All", ...new Set(state.records.map(record => record.category))];
|
|
}
|
|
|
|
function filteredRecords() {
|
|
return state.records.filter(record => {
|
|
const categoryMatch = state.category === "All" || record.category === state.category;
|
|
const statusText = record.has_local_preview ? "local preview ready" : "not synced locally";
|
|
const haystack = `${record.slug} ${record.category} ${record.brief} ${statusText}`.toLowerCase();
|
|
const queryMatch = !state.query || haystack.includes(state.query);
|
|
return categoryMatch && queryMatch;
|
|
});
|
|
}
|
|
|
|
function selectedRecord(records) {
|
|
const current = records.find(record => record.slug === state.selected);
|
|
if (current) return current;
|
|
if (records[0]) {
|
|
state.selected = records[0].slug;
|
|
return records[0];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function previewHref(record) {
|
|
return state.mode === "dark" ? record.dark_url : record.light_url;
|
|
}
|
|
|
|
function render() {
|
|
const records = filteredRecords();
|
|
const selected = selectedRecord(records);
|
|
renderChips();
|
|
renderList(records);
|
|
renderDetail(selected);
|
|
}
|
|
|
|
function renderChips() {
|
|
refs.chips.innerHTML = categories()
|
|
.map(category => `
|
|
<button
|
|
class="chip ${state.category === category ? "active" : ""}"
|
|
data-category="${escapeHtml(category)}"
|
|
type="button"
|
|
>${escapeHtml(category)}</button>
|
|
`)
|
|
.join("");
|
|
}
|
|
|
|
function renderList(records) {
|
|
if (!records.length) {
|
|
refs.list.innerHTML = '<div class="empty-state">No designs match the current filter.</div>';
|
|
return;
|
|
}
|
|
|
|
refs.list.innerHTML = records
|
|
.map(record => `
|
|
<article class="design-card ${state.selected === record.slug ? "active" : ""}" data-slug="${escapeHtml(record.slug)}">
|
|
<h3 class="design-name">${escapeHtml(record.slug)}</h3>
|
|
<p class="design-category">${escapeHtml(record.category)}</p>
|
|
<p class="design-brief">${escapeHtml(record.brief)}</p>
|
|
<span class="design-status">${record.has_local_preview ? "Local preview ready" : "Not synced locally"}</span>
|
|
</article>
|
|
`)
|
|
.join("");
|
|
}
|
|
|
|
function renderDetail(record) {
|
|
if (!record) {
|
|
refs.heroTitle.textContent = "No design selected";
|
|
refs.heroSub.textContent = "Try a different search or category.";
|
|
refs.heroBrief.textContent = "The local preview site links directly to generated files under designs/<slug>/.";
|
|
refs.previewLabel.textContent = "Preview";
|
|
refs.previewPath.textContent = "-";
|
|
refs.previewPath.href = "#";
|
|
refs.previewFrame.removeAttribute("src");
|
|
refs.actionMd.href = "#";
|
|
refs.actionSource.href = "#";
|
|
refs.actionStatus.textContent = "No selection";
|
|
setPreviewActionsEnabled(false);
|
|
updateModeButtons();
|
|
return;
|
|
}
|
|
|
|
refs.heroTitle.textContent = record.slug;
|
|
refs.heroSub.textContent = `${record.category} ยท ${record.has_local_preview ? "local quick preview" : "catalog entry only"}`;
|
|
refs.heroBrief.textContent = record.brief;
|
|
refs.actionSource.href = record.source_url;
|
|
refs.actionStatus.textContent = record.has_local_preview ? "Local Preview Ready" : "Not Synced Locally";
|
|
|
|
if (record.has_local_preview) {
|
|
const href = previewHref(record);
|
|
refs.previewLabel.textContent = state.mode === "dark" ? "Dark Preview" : "Light Preview";
|
|
refs.previewPath.textContent = href;
|
|
refs.previewPath.href = href;
|
|
refs.previewFrame.src = href;
|
|
refs.actionMd.href = record.design_md_url;
|
|
setPreviewActionsEnabled(true);
|
|
} else {
|
|
refs.previewLabel.textContent = "Preview unavailable";
|
|
refs.previewPath.textContent = "Sync this design locally to preview it";
|
|
refs.previewPath.href = record.source_url;
|
|
refs.previewFrame.removeAttribute("src");
|
|
refs.actionMd.href = "#";
|
|
setPreviewActionsEnabled(false);
|
|
}
|
|
|
|
updateModeButtons();
|
|
}
|
|
|
|
function updateModeButtons() {
|
|
refs.actionLight.classList.toggle("action-button-primary", state.mode === "light");
|
|
refs.actionDark.classList.toggle("action-button-primary", state.mode === "dark");
|
|
refs.actionLight.classList.toggle("action-button-active", state.mode === "light");
|
|
refs.actionDark.classList.toggle("action-button-active", state.mode === "dark");
|
|
}
|
|
|
|
function setPreviewActionsEnabled(enabled) {
|
|
refs.actionLight.classList.toggle("action-button-disabled", !enabled);
|
|
refs.actionDark.classList.toggle("action-button-disabled", !enabled);
|
|
refs.actionMd.classList.toggle("action-button-disabled", !enabled);
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return value
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
bootstrap();
|