const httpService = { baseUrl: window.GITEA_SUB_URL || "", async fetchRss() { const resp = await fetch(`${this.baseUrl}/alex.rss`); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const text = await resp.text(); return new DOMParser().parseFromString(text, "application/xml"); }, }; const dataDomain = { timeAgo(dateStr) { const diff = (Date.now() - new Date(dateStr)) / 1000; if (diff < 60) return "just now"; if (diff < 3600) return Math.floor(diff / 60) + "m ago"; if (diff < 86400) return Math.floor(diff / 3600) + "h ago"; if (diff < 2592000) return Math.floor(diff / 86400) + "d ago"; if (diff < 31536000) return Math.floor(diff / 2592000) + "mo ago"; return Math.floor(diff / 31536000) + "y ago"; }, esc(str) { return (str || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); }, safeTitleHtml(rawTitleText) { const doc = new DOMParser().parseFromString(rawTitleText, "text/html"); doc.body .querySelectorAll("*:not(a)") .forEach((el) => el.replaceWith(el.textContent)); return doc.body.innerHTML; }, titlePlainText(rawTitleText) { const doc = new DOMParser().parseFromString(rawTitleText, "text/html"); return doc.body.textContent || rawTitleText; }, activityIcon(titleText) { const t = titleText.toLowerCase(); if (t.includes("push") || t.includes("commit")) return "📤"; if (t.includes("creat") && t.includes("repo")) return "📁"; if (t.includes("fork")) return "🍴"; if (t.includes("open") && t.includes("issue")) return "🔴"; if (t.includes("clos") && t.includes("issue")) return "🟢"; if (t.includes("pull request") || t.includes("merge")) return "🔀"; if (t.includes("tag")) return "🏷️"; if (t.includes("branch")) return "🌿"; if (t.includes("comment")) return "💬"; if (t.includes("release")) return "🚀"; return "⚡"; }, parseCommits(descriptionText) { const doc = new DOMParser().parseFromString(descriptionText, "text/html"); return Array.from(doc.querySelectorAll("a")).map((anchor) => { const sha = anchor.textContent.trim().slice(0, 7); const href = anchor.getAttribute("href") || "#"; let msg = ""; let node = anchor.nextSibling; while (node) { const t = (node.textContent || "").trim(); if (t) { msg = t; break; } node = node.nextSibling; } return { sha, href, msg }; }); }, parseRepos(xmlDoc) { const items = Array.from(xmlDoc.querySelectorAll("channel > item")); const seen = new Map(); for (const item of items) { const titleHtml = item.querySelector("title")?.textContent || ""; const titleDoc = new DOMParser().parseFromString(titleHtml, "text/html"); const anchors = titleDoc.querySelectorAll("a"); if (anchors.length < 2) continue; const repoAnchor = anchors[anchors.length - 1]; const repoName = repoAnchor.textContent.trim(); if (!repoName || seen.has(repoName)) continue; seen.set(repoName, { repoName, repoUrl: repoAnchor.getAttribute("href") || "#", shortName: repoName.includes("/") ? repoName.split("/").pop() : repoName, pubDate: item.querySelector("pubDate")?.textContent || "", firstCommit: this.parseCommits( item.querySelector("description")?.textContent || "", )[0] || null, }); } return Array.from(seen.values()); }, parseActivity(xmlDoc, limit = 20) { return Array.from(xmlDoc.querySelectorAll("channel > item")) .slice(0, limit) .map((item) => { const rawTitle = item.querySelector("title")?.textContent || ""; const titleText = this.titlePlainText(rawTitle); return { titleHtmlSafe: this.safeTitleHtml(rawTitle), titleText, link: item.querySelector("link")?.textContent || "#", pubDate: item.querySelector("pubDate")?.textContent || "", icon: this.activityIcon(titleText), commits: this.parseCommits( item.querySelector("description")?.textContent || "", ).slice(0, 3), }; }); }, }; const uiRendering = { async renderRepos(xmlDoc) { const grid = document.getElementById("repo-grid"); if (!grid) return; const repos = dataDomain.parseRepos(xmlDoc); if (repos.length === 0) { grid.innerHTML = `
No repositories found in feed.
`; return; } grid.innerHTML = ""; for (const { shortName, repoName, repoUrl, pubDate, firstCommit, } of repos) { const when = dataDomain.timeAgo(pubDate); const commitMsg = firstCommit?.msg || firstCommit?.sha || ""; const card = document.createElement("a"); card.className = "repo-card"; card.href = dataDomain.esc(repoUrl); card.innerHTML = `
📦 ${dataDomain.esc(shortName)}
${dataDomain.esc(repoName)}
${dataDomain.esc(commitMsg)} ${dataDomain.esc(when)}
`.trim(); grid.appendChild(card); } }, async renderActivity(xmlDoc) { const feed = document.getElementById("activity-feed"); if (!feed) return; const items = dataDomain.parseActivity(xmlDoc); if (items.length === 0) { feed.innerHTML = `
No public activity yet.
`; return; } feed.innerHTML = ""; for (const { titleHtmlSafe, icon, pubDate, commits } of items) { const when = dataDomain.timeAgo(pubDate); const commitsHtml = commits.length === 0 ? "" : `
` + commits .map( ({ sha, href, msg }) => `
${dataDomain.esc(sha)} ${dataDomain.esc(msg)}
`, ) .join("") + `
`; const el = document.createElement("div"); el.className = "activity-item"; el.innerHTML = `
${icon}
${titleHtmlSafe}
${when}
${commitsHtml}
`; feed.appendChild(el); } }, async render() { const baseUrl = httpService.baseUrl; try { const xmlDoc = await httpService.fetchRss(); await Promise.all([ this.renderRepos(xmlDoc), this.renderActivity(xmlDoc), ]); } catch (e) { console.error("Gitea landing: RSS fetch failed", e); const grid = document.getElementById("repo-grid"); const feed = document.getElementById("activity-feed"); if (grid) grid.innerHTML = `
Could not load feed (${e.message}). Browse manually →
`; if (feed) feed.innerHTML = `
Could not load activity (${e.message})
`; return; } }, }; document.addEventListener("DOMContentLoaded", () => uiRendering.render());