(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 setMetaContent = (selector, content) => { const element = document.querySelector(selector); if (!element || !content) { return; } element.setAttribute("content", content); }; const setCanonicalHref = (href) => { const canonical = document.querySelector("link[rel='canonical']"); if (!canonical || !href) { return; } canonical.setAttribute("href", href); }; const setEntryJsonLd = (entry, url, image) => { let script = document.getElementById("entry-jsonld"); if (!script) { script = document.createElement("script"); script.id = "entry-jsonld"; script.type = "application/ld+json"; document.head.appendChild(script); } script.textContent = JSON.stringify({ "@context": "https://schema.org", "@type": "Article", "@id": `${url}#article`, "url": url, "headline": entry.title, "description": entry.summary || "Project entry or update from Braeden Sowinski.", "datePublished": entry.date || undefined, "dateModified": entry.date || undefined, "image": image, "author": { "@id": "https://sowinski.dev/#person", "name": "Braeden Sowinski", "url": "https://sowinski.dev/" }, "publisher": { "@id": "https://sowinski.dev/#person" }, "mainEntityOfPage": url, "isPartOf": { "@id": "https://sowinski.dev/#website" } }); }; const updateEntryMeta = (entry) => { const baseUrl = "https://sowinski.dev"; const title = `${entry.title} | Braeden Sowinski`; const description = entry.summary || "Project entry or update from Braeden Sowinski."; const url = `${baseUrl}/entry.html?type=${encodeURIComponent(entry.type)}&slug=${encodeURIComponent(entry.slug)}`; const image = "https://avatars.githubusercontent.com/u/48144618?v=4"; document.title = title; setCanonicalHref(url); setMetaContent("meta[name='description']", description); setMetaContent("meta[property='og:title']", title); setMetaContent("meta[property='og:description']", description); setMetaContent("meta[property='og:url']", url); setMetaContent("meta[property='og:image']", image); setMetaContent("meta[name='twitter:title']", title); setMetaContent("meta[name='twitter:description']", description); setMetaContent("meta[name='twitter:image']", image); setEntryJsonLd(entry, url, image); }; const renderLinks = (entry, className) => { const links = []; const sourceLabel = entry.github && entry.github.includes("git.sowinski.dev") ? "Gitea" : "GitHub"; links.push(`Read More`); if (entry.demo) { links.push(`Live`); } else if (entry.github) { links.push(`${sourceLabel}`); } return `
${links.join("")}
`; }; const createCard = (entry) => { const formattedDate = formatDate(entry.date); const featuredClass = entry.featured ? " featured" : ""; const anchorId = entry.slug ? ` id="${escapeHtml(entry.slug)}"` : ""; 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 initProjectsToggle = (entries) => { const projectsGrid = document.getElementById("projects-grid"); const toggleShell = document.getElementById("projects-toggle-shell"); const toggleButton = document.getElementById("projects-toggle"); const defaultVisibleCount = 6; if (!projectsGrid || !toggleShell || !toggleButton) { return; } const cards = Array.from(projectsGrid.querySelectorAll(".content-card")); if (entries.length <= defaultVisibleCount || cards.length <= defaultVisibleCount) { toggleShell.hidden = true; return; } const setExpandedState = (expanded) => { cards.forEach((card, index) => { card.classList.toggle("is-hidden", !expanded && index >= defaultVisibleCount); }); toggleButton.textContent = expanded ? "Show Fewer Projects" : "Show More Projects"; toggleButton.setAttribute("aria-expanded", String(expanded)); }; toggleShell.hidden = false; setExpandedState(false); toggleButton.addEventListener("click", () => { const expanded = toggleButton.getAttribute("aria-expanded") === "true"; setExpandedState(!expanded); }); }; const compareByNewestDate = (left, right) => { const leftTime = left.date ? Date.parse(`${left.date}T00:00:00`) : Number.NEGATIVE_INFINITY; const rightTime = right.date ? Date.parse(`${right.date}T00:00:00`) : Number.NEGATIVE_INFINITY; if (leftTime !== rightTime) { return rightTime - leftTime; } const leftOrder = Number.isFinite(left.order) ? left.order : Number.MAX_SAFE_INTEGER; const rightOrder = Number.isFinite(right.order) ? right.order : Number.MAX_SAFE_INTEGER; if (leftOrder !== rightOrder) { return leftOrder - rightOrder; } return String(left.title || "").localeCompare(String(right.title || "")); }; const compareByPriorityOrder = (left, right) => { const leftOrder = Number.isFinite(left.order) ? left.order : Number.MAX_SAFE_INTEGER; const rightOrder = Number.isFinite(right.order) ? right.order : Number.MAX_SAFE_INTEGER; if (leftOrder !== rightOrder) { return leftOrder - rightOrder; } const leftTime = left.date ? Date.parse(`${left.date}T00:00:00`) : Number.NEGATIVE_INFINITY; const rightTime = right.date ? Date.parse(`${right.date}T00:00:00`) : Number.NEGATIVE_INFINITY; if (leftTime !== rightTime) { return rightTime - leftTime; } return String(left.title || "").localeCompare(String(right.title || "")); }; const initListPage = async () => { try { const index = await getIndex(); const sortedProjects = [...(index.projects || [])].sort(compareByPriorityOrder); renderSection("projects-grid", sortedProjects, "No projects added yet."); initProjectsToggle(sortedProjects); renderSection("updates-grid", [...(index.updates || [])].sort(compareByNewestDate), "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."); } updateEntryMeta(entry); 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.demo ? `Live` : ""} ${entry.github ? `${entry.github.includes("git.sowinski.dev") ? "Gitea" : "GitHub"}` : ""}
${window.marked.parse(markdown)}
`; } catch (error) { shell.innerHTML = `

Not found

${escapeHtml(error.message)}

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