(function() { const CONTENT_INDEX_PATH = "./public/content/index.json"; const escapeHtml = (value) => String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); const formatDate = (value) => { if (!value) { return ""; } const date = new Date(`${value}T00:00:00`); if (Number.isNaN(date.getTime())) { return value; } return new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "short", day: "numeric" }).format(date); }; const getIndex = async () => { if (window.location.protocol === "file:") { throw new Error("This content page needs a local web server. Run python3 -m http.server and open http://localhost:8000."); } const response = await fetch(CONTENT_INDEX_PATH); if (!response.ok) { throw new Error("Unable to load content index."); } return response.json(); }; const stripFrontMatter = (markdown) => { if (!markdown.startsWith("---")) { return markdown; } const parts = markdown.split("---"); if (parts.length < 3) { return markdown; } return parts.slice(2).join("---").trim(); }; const renderTags = (tags, className) => { if (!tags || !tags.length) { return ""; } return `
${tags.map((tag) => `${escapeHtml(tag)}`).join("")}
`; }; const renderLinks = (entry, className) => { const links = []; links.push(`Read More`); if (entry.github) { links.push(`Github`); } if (entry.demo) { links.push(`Live`); } return `
${links.join("")}
`; }; const createCard = (entry) => { const formattedDate = formatDate(entry.date); const featuredClass = entry.featured ? " featured" : ""; return `
${escapeHtml(entry.label || entry.type)} ${formattedDate ? `${escapeHtml(formattedDate)}` : ""}

${escapeHtml(entry.title)}

${escapeHtml(entry.summary || "")}

${renderTags(entry.tags || [], "tag-list")} ${renderLinks(entry, "card-link")}
`; }; const renderSection = (containerId, entries, emptyMessage) => { const container = document.getElementById(containerId); if (!container) { return; } if (!entries.length) { container.innerHTML = `

${escapeHtml(emptyMessage)}

`; return; } container.innerHTML = entries.map(createCard).join(""); }; const initListPage = async () => { try { const index = await getIndex(); renderSection("projects-grid", index.projects || [], "No projects added yet."); renderSection("updates-grid", index.updates || [], "No updates added yet."); } catch (error) { renderSection("projects-grid", [], error.message); renderSection("updates-grid", [], error.message); } }; const initEntryPage = async () => { const shell = document.getElementById("entry-shell"); if (!shell) { return; } try { const params = new URLSearchParams(window.location.search); const type = params.get("type"); const slug = params.get("slug"); if (!type || !slug) { shell.innerHTML = `

Not found

This entry could not be loaded.

Back to projects
`; return; } const index = await getIndex(); const entries = [...(index.projects || []), ...(index.updates || [])]; const entry = entries.find((item) => item.type === type && item.slug === slug); if (!entry) { throw new Error("Entry not found."); } const response = await fetch(entry.path); if (!response.ok) { throw new Error("Unable to load markdown entry."); } const rawMarkdown = await response.text(); const markdown = stripFrontMatter(rawMarkdown); shell.innerHTML = `
Back to projects
${escapeHtml(entry.label || entry.type)} ${entry.date ? `${escapeHtml(formatDate(entry.date))}` : ""} ${entry.status ? `${escapeHtml(entry.status)}` : ""}

${escapeHtml(entry.title)}

${escapeHtml(entry.summary || "")}

${renderTags(entry.tags || [], "entry-tag-list")}
${entry.github ? `Github` : ""} ${entry.demo ? `Live` : ""}
${window.marked.parse(markdown)}
`; } catch (error) { shell.innerHTML = `

Not found

${escapeHtml(error.message)}

Back to projects
`; } }; window.PORTFOLIO_CONTENT = { initListPage, initEntryPage }; })();