landing updates
All checks were successful
Apply Kuberentes Configs / update-repo (push) Successful in 1s
Apply Kuberentes Configs / update-infrastructure (push) Successful in 4s
Apply Kuberentes Configs / notify-on-failure (push) Has been skipped

This commit is contained in:
2026-03-05 10:47:55 -07:00
parent 4d6357cc74
commit 567a59f9b1
3 changed files with 103 additions and 84 deletions

View File

@@ -84,10 +84,7 @@
margin: 0; margin: 0;
color: var(--color-text); color: var(--color-text);
} }
.subtitle {
font-size: 0.9rem;
color: var(--color-text-muted);
}
/* Grid */ /* Grid */
.repo-grid { .repo-grid {

View File

@@ -10,7 +10,6 @@
<section class="projects-section"> <section class="projects-section">
<div class="section-header"> <div class="section-header">
<h2>Recent Projects</h2> <h2>Recent Projects</h2>
<span class="subtitle">Latest commits across all repositories</span>
</div> </div>
<div id="repo-grid" class="repo-grid"> <div id="repo-grid" class="repo-grid">
<div class="skeleton-card"></div> <div class="skeleton-card"></div>
@@ -25,7 +24,6 @@
<section class="activity-section"> <section class="activity-section">
<div class="section-header"> <div class="section-header">
<h2>Recent Activity</h2> <h2>Recent Activity</h2>
<span class="subtitle">What Alex has been up to</span>
<a href="/alex" class="view-all-link">View full profile →</a> <a href="/alex" class="view-all-link">View full profile →</a>
</div> </div>
<div id="activity-feed" class="activity-feed"> <div id="activity-feed" class="activity-feed">
@@ -42,6 +40,7 @@
window.GITEA_APP_URL = "{{AppUrl}}"; window.GITEA_APP_URL = "{{AppUrl}}";
window.GITEA_SUB_URL = "{{AppSubUrl}}"; window.GITEA_SUB_URL = "{{AppSubUrl}}";
</script> </script>
<script src="{{AppSubUrl}}/assets/js/custom-landing.js?v=4"></script> <!-- update version when changed to reset cloudflare cache -->
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/custom-landing.css?v=4"> <script src="{{AppSubUrl}}/assets/js/custom-landing.js?v=5"></script>
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/custom-landing.css?v=5">
{{template "base/footer" .}} {{template "base/footer" .}}

View File

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