landing updates
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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" .}}
|
||||||
|
|||||||
@@ -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, '&')
|
.replace(/&/g, "&")
|
||||||
.replace(/</g, '<')
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, ">")
|
||||||
.replace(/"/g, '"');
|
.replace(/"/g, """);
|
||||||
},
|
},
|
||||||
|
|
||||||
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>`,
|
||||||
|
)
|
||||||
|
.join("") +
|
||||||
`</div>`;
|
`</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();
|
||||||
} 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 = `<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.renderRepos(xmlDoc),
|
this.renderRepos(xmlDoc),
|
||||||
this.renderActivity(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 = `<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;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => uiRendering.render());
|
document.addEventListener("DOMContentLoaded", () => uiRendering.render());
|
||||||
|
|||||||
Reference in New Issue
Block a user