Files
chuan 14e8087aac feat: add design preview site with search and filtering functionality
- 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.
2026-04-20 23:18:02 +08:00

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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
bootstrap();