landing updates
This commit is contained in:
@@ -329,3 +329,30 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Heatmap ─────────────────────────────────────────────── */
|
||||||
|
.heatmap-section {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 var(--space-lg) var(--space-xl);
|
||||||
|
}
|
||||||
|
.activity-heatmap {
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.heatmap-svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.heatmap-month {
|
||||||
|
font-size: 9px;
|
||||||
|
fill: var(--color-text-muted, #8b949e);
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.heatmap-day {
|
||||||
|
font-size: 9px;
|
||||||
|
fill: var(--color-text-muted, #8b949e);
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{{template "base/head" .}}
|
{{template "base/head" .}}
|
||||||
<div class="page-content home" id="alex-landing">
|
<div class="page-content home" id="alex-landing">
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="hero-inner">
|
<div class="hero-inner">
|
||||||
<h1>Alex Mickelson</h1>
|
<h1>Alex Mickelson</h1>
|
||||||
@@ -21,6 +20,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="heatmap-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Activity</h2>
|
||||||
|
</div>
|
||||||
|
<div id="activity-heatmap" class="activity-heatmap"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="activity-section">
|
<section class="activity-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Recent Activity</h2>
|
<h2>Recent Activity</h2>
|
||||||
@@ -34,13 +40,12 @@
|
|||||||
<div class="skeleton-activity"></div>
|
<div class="skeleton-activity"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
window.GITEA_APP_URL = "{{AppUrl}}";
|
window.GITEA_APP_URL = "{{AppUrl}}";
|
||||||
window.GITEA_SUB_URL = "{{AppSubUrl}}";
|
window.GITEA_SUB_URL = "{{AppSubUrl}}";
|
||||||
</script>
|
</script>
|
||||||
<!-- update version when changed to reset cloudflare cache -->
|
<!-- update version when changed to reset cloudflare cache -->
|
||||||
<script src="{{AppSubUrl}}/assets/js/custom-landing.js?v=7"></script>
|
<script src="{{AppSubUrl}}/assets/js/custom-landing.js?v=8"></script>
|
||||||
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/custom-landing.css?v=7">
|
<link href="{{AppSubUrl}}/assets/css/custom-landing.css?v=8" rel="stylesheet" />
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
|||||||
@@ -102,6 +102,19 @@ const dataDomain = {
|
|||||||
return Array.from(seen.values());
|
return Array.from(seen.values());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
parseAllActivityDates(xmlDoc) {
|
||||||
|
const counts = new Map();
|
||||||
|
for (const item of Array.from(xmlDoc.querySelectorAll("channel > item"))) {
|
||||||
|
const pubDate = item.querySelector("pubDate")?.textContent || "";
|
||||||
|
if (!pubDate) continue;
|
||||||
|
const d = new Date(pubDate);
|
||||||
|
if (isNaN(d)) continue;
|
||||||
|
const key = d.toISOString().slice(0, 10);
|
||||||
|
counts.set(key, (counts.get(key) || 0) + 1);
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
},
|
||||||
|
|
||||||
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)
|
||||||
@@ -208,6 +221,135 @@ const uiRendering = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
activityMapRender(xmlDoc) {
|
||||||
|
const container = document.getElementById("activity-heatmap");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const counts = dataDomain.parseAllActivityDates(xmlDoc);
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Align start to Sunday 52 weeks ago
|
||||||
|
const startDate = new Date(today);
|
||||||
|
startDate.setDate(startDate.getDate() - 52 * 7);
|
||||||
|
startDate.setDate(startDate.getDate() - startDate.getDay());
|
||||||
|
|
||||||
|
const cellSize = 11;
|
||||||
|
const gap = 2;
|
||||||
|
const step = cellSize + gap;
|
||||||
|
const cols = 53;
|
||||||
|
const rows = 7;
|
||||||
|
const padLeft = 28;
|
||||||
|
const padTop = 20;
|
||||||
|
const svgW = padLeft + cols * step;
|
||||||
|
const svgH = padTop + rows * step;
|
||||||
|
|
||||||
|
const LEVELS = ["#161b22", "#0e4429", "#006d32", "#26a641", "#39d353"];
|
||||||
|
const countToLevel = (n) =>
|
||||||
|
n === 0 ? 0 : n === 1 ? 1 : n <= 3 ? 2 : n <= 6 ? 3 : 4;
|
||||||
|
|
||||||
|
// Collect month labels (one per column where the month changes)
|
||||||
|
const monthLabels = new Map();
|
||||||
|
let lastMonth = -1;
|
||||||
|
for (let col = 0; col < cols; col++) {
|
||||||
|
const d = new Date(startDate);
|
||||||
|
d.setDate(d.getDate() + col * 7);
|
||||||
|
if (d.getMonth() !== lastMonth) {
|
||||||
|
lastMonth = d.getMonth();
|
||||||
|
monthLabels.set(col, d.toLocaleString("default", { month: "short" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ns = "http://www.w3.org/2000/svg";
|
||||||
|
const svg = document.createElementNS(ns, "svg");
|
||||||
|
svg.setAttribute("width", svgW);
|
||||||
|
svg.setAttribute("height", svgH);
|
||||||
|
svg.setAttribute("class", "heatmap-svg");
|
||||||
|
svg.setAttribute("aria-label", "Activity heatmap");
|
||||||
|
|
||||||
|
// Month labels
|
||||||
|
for (const [col, name] of monthLabels) {
|
||||||
|
const t = document.createElementNS(ns, "text");
|
||||||
|
t.setAttribute("x", padLeft + col * step);
|
||||||
|
t.setAttribute("y", 12);
|
||||||
|
t.setAttribute("class", "heatmap-month");
|
||||||
|
t.textContent = name;
|
||||||
|
svg.appendChild(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Day-of-week labels (Sun / Tue / Thu / Sat)
|
||||||
|
["Sun", "", "Tue", "", "Thu", "", "Sat"].forEach((label, i) => {
|
||||||
|
if (!label) return;
|
||||||
|
const t = document.createElementNS(ns, "text");
|
||||||
|
t.setAttribute("x", 0);
|
||||||
|
t.setAttribute("y", padTop + i * step + cellSize - 2);
|
||||||
|
t.setAttribute("class", "heatmap-day");
|
||||||
|
t.textContent = label;
|
||||||
|
svg.appendChild(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Day cells
|
||||||
|
for (let col = 0; col < cols; col++) {
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
const d = new Date(startDate);
|
||||||
|
d.setDate(d.getDate() + col * 7 + row);
|
||||||
|
if (d > today) continue;
|
||||||
|
|
||||||
|
const key = d.toISOString().slice(0, 10);
|
||||||
|
const count = counts.get(key) || 0;
|
||||||
|
|
||||||
|
const rect = document.createElementNS(ns, "rect");
|
||||||
|
rect.setAttribute("x", padLeft + col * step);
|
||||||
|
rect.setAttribute("y", padTop + row * step);
|
||||||
|
rect.setAttribute("width", cellSize);
|
||||||
|
rect.setAttribute("height", cellSize);
|
||||||
|
rect.setAttribute("rx", 2);
|
||||||
|
rect.setAttribute("fill", LEVELS[countToLevel(count)]);
|
||||||
|
rect.setAttribute("data-date", key);
|
||||||
|
rect.setAttribute("data-count", count);
|
||||||
|
|
||||||
|
const title = document.createElementNS(ns, "title");
|
||||||
|
title.textContent = count > 0
|
||||||
|
? `${count} activit${count === 1 ? "y" : "ies"} on ${key}`
|
||||||
|
: `No activity on ${key}`;
|
||||||
|
rect.appendChild(title);
|
||||||
|
svg.appendChild(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legend
|
||||||
|
const legendY = svgH + 6;
|
||||||
|
const legendG = document.createElementNS(ns, "g");
|
||||||
|
const legendLabel = document.createElementNS(ns, "text");
|
||||||
|
legendLabel.setAttribute("x", padLeft);
|
||||||
|
legendLabel.setAttribute("y", legendY + cellSize - 2);
|
||||||
|
legendLabel.setAttribute("class", "heatmap-day");
|
||||||
|
legendLabel.textContent = "Less";
|
||||||
|
legendG.appendChild(legendLabel);
|
||||||
|
LEVELS.forEach((color, i) => {
|
||||||
|
const r = document.createElementNS(ns, "rect");
|
||||||
|
r.setAttribute("x", padLeft + 32 + i * step);
|
||||||
|
r.setAttribute("y", legendY);
|
||||||
|
r.setAttribute("width", cellSize);
|
||||||
|
r.setAttribute("height", cellSize);
|
||||||
|
r.setAttribute("rx", 2);
|
||||||
|
r.setAttribute("fill", color);
|
||||||
|
legendG.appendChild(r);
|
||||||
|
});
|
||||||
|
const moreLabel = document.createElementNS(ns, "text");
|
||||||
|
moreLabel.setAttribute("x", padLeft + 32 + LEVELS.length * step + 4);
|
||||||
|
moreLabel.setAttribute("y", legendY + cellSize - 2);
|
||||||
|
moreLabel.setAttribute("class", "heatmap-day");
|
||||||
|
moreLabel.textContent = "More";
|
||||||
|
legendG.appendChild(moreLabel);
|
||||||
|
svg.setAttribute("height", svgH + cellSize + 12);
|
||||||
|
svg.appendChild(legendG);
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
container.appendChild(svg);
|
||||||
|
},
|
||||||
|
|
||||||
async render() {
|
async render() {
|
||||||
const baseUrl = httpService.baseUrl;
|
const baseUrl = httpService.baseUrl;
|
||||||
|
|
||||||
@@ -216,6 +358,7 @@ const uiRendering = {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.renderRepos(xmlDoc),
|
this.renderRepos(xmlDoc),
|
||||||
this.renderActivity(xmlDoc),
|
this.renderActivity(xmlDoc),
|
||||||
|
this.activityMapRender(xmlDoc),
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Gitea landing: RSS fetch failed", e);
|
console.error("Gitea landing: RSS fetch failed", e);
|
||||||
|
|||||||
Reference in New Issue
Block a user