Files
infrastructure/kubernetes/gitea/landingpage.yml
Alex Mickelson e32f08391b
All checks were successful
Apply Kuberentes Configs / update-repo (push) Successful in 1s
Apply Kuberentes Configs / update-infrastructure (push) Successful in 3s
Apply Kuberentes Configs / notify-on-failure (push) Has been skipped
new page
2026-03-05 10:17:15 -07:00

581 lines
18 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
apiVersion: v1
kind: ConfigMap
metadata:
name: gitea-landing-page
namespace: gitea
data:
home.tmpl: |
{{template "base/head" .}}
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/custom-landing.css">
<div class="page-content home" id="alex-landing">
<section class="hero">
<div class="hero-inner">
<h1>Alex Mickelson</h1>
<div class="hero-links">
<a href="/explore/repos" class="btn-primary">Browse All Projects</a>
<a href="/user/login" class="btn-ghost">Sign In</a>
</div>
</div>
</section>
<section class="projects-section">
<div class="section-header">
<h2>Recent Projects</h2>
<span class="subtitle">Latest commits across all repositories</span>
</div>
<div id="repo-grid" class="repo-grid">
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
</div>
</section>
<section class="activity-section">
<div class="section-header">
<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>
</div>
<div id="activity-feed" class="activity-feed">
<div class="skeleton-activity"></div>
<div class="skeleton-activity"></div>
<div class="skeleton-activity"></div>
<div class="skeleton-activity"></div>
<div class="skeleton-activity"></div>
</div>
</section>
</div>
<script>
window.GITEA_APP_URL = "{{AppUrl}}";
window.GITEA_SUB_URL = "{{AppSubUrl}}";
</script>
<script src="{{AppSubUrl}}/assets/js/custom-landing.js"></script>
{{template "base/footer" .}}
custom-landing.css: |
#alex-landing {
min-height: 100vh;
background: #0d1117;
color: #e6edf3;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* Hero */
.hero {
display: flex;
justify-content: center;
align-items: center;
padding: 80px 24px 60px;
text-align: center;
}
.hero-inner {
max-width: 640px;
}
.hero h1 {
font-size: 3rem;
font-weight: 800;
margin: 0 0 12px;
background: linear-gradient(135deg, #e6edf3 0%, #58a6ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-links {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.btn-primary {
padding: 10px 24px;
border-radius: 8px;
background: #238636;
color: #fff;
font-weight: 600;
font-size: 0.95rem;
text-decoration: none;
transition: background 0.2s;
}
.btn-primary:hover { background: #2ea043; }
.btn-ghost {
padding: 10px 24px;
border-radius: 8px;
border: 1px solid #30363d;
color: #e6edf3;
font-weight: 600;
font-size: 0.95rem;
text-decoration: none;
transition: border-color 0.2s, background 0.2s;
}
.btn-ghost:hover { border-color: #58a6ff; background: #58a6ff11; }
/* Projects section */
.projects-section {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px 80px;
}
.section-header {
margin-bottom: 24px;
display: flex;
align-items: baseline;
gap: 12px;
flex-wrap: wrap;
}
.section-header h2 {
font-size: 1.5rem;
font-weight: 700;
margin: 0;
color: #e6edf3;
}
.subtitle {
font-size: 0.9rem;
color: #8b949e;
}
/* Grid */
.repo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
/* Skeleton loaders */
.skeleton-card {
height: 160px;
border-radius: 12px;
background: linear-gradient(90deg, #161b22 25%, #21262d 50%, #161b22 75%);
background-size: 200% 100%;
animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Repo cards */
.repo-card {
background: #161b22;
border: 1px solid #21262d;
border-radius: 12px;
padding: 20px;
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
gap: 10px;
transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.repo-card:hover {
border-color: #58a6ff;
transform: translateY(-2px);
box-shadow: 0 8px 24px #58a6ff1a;
}
.repo-card-header {
display: flex;
align-items: center;
gap: 10px;
}
.repo-icon {
font-size: 1.1rem;
flex-shrink: 0;
}
.repo-name {
font-size: 1rem;
font-weight: 600;
color: #58a6ff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.repo-private {
font-size: 0.7rem;
background: #21262d;
border: 1px solid #30363d;
border-radius: 4px;
padding: 1px 6px;
color: #8b949e;
flex-shrink: 0;
}
.repo-desc {
font-size: 0.875rem;
color: #8b949e;
line-height: 1.5;
flex: 1;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.repo-commit {
font-size: 0.8rem;
color: #6e7681;
border-top: 1px solid #21262d;
padding-top: 10px;
display: flex;
align-items: center;
gap: 6px;
overflow: hidden;
}
.commit-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #238636;
flex-shrink: 0;
}
.commit-msg {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.commit-time {
color: #484f58;
flex-shrink: 0;
}
.repo-meta {
display: flex;
gap: 14px;
font-size: 0.8rem;
color: #6e7681;
}
.repo-meta span { display: flex; align-items: center; gap: 4px; }
/* Error state */
.error-msg {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: #8b949e;
font-size: 0.95rem;
}
/* Activity section */
.activity-section {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px 80px;
}
.view-all-link {
font-size: 0.85rem;
color: #58a6ff;
text-decoration: none;
margin-left: auto;
}
.view-all-link:hover { text-decoration: underline; }
.activity-feed {
display: flex;
flex-direction: column;
gap: 0;
border: 1px solid #21262d;
border-radius: 12px;
overflow: hidden;
}
.skeleton-activity {
height: 52px;
background: linear-gradient(90deg, #161b22 25%, #21262d 50%, #161b22 75%);
background-size: 200% 100%;
animation: shimmer 1.4s infinite;
border-top: 1px solid #0d1117;
}
.skeleton-activity:first-child { border-top: none; }
.activity-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
background: #161b22;
border-top: 1px solid #21262d;
font-size: 0.875rem;
transition: background 0.15s;
}
.activity-item:first-child { border-top: none; }
.activity-item:hover { background: #1c2128; }
.activity-op-icon {
flex-shrink: 0;
width: 28px;
height: 28px;
border-radius: 50%;
background: #21262d;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
margin-top: 1px;
}
.activity-body { flex: 1; min-width: 0; }
.activity-headline { color: #e6edf3; line-height: 1.5; }
.activity-headline a {
color: #58a6ff;
text-decoration: none;
font-weight: 600;
}
.activity-headline a:hover { text-decoration: underline; }
.activity-commits {
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 3px;
}
.activity-commit-line {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
color: #8b949e;
overflow: hidden;
}
.activity-commit-sha {
font-family: monospace;
font-size: 0.75rem;
color: #58a6ff;
flex-shrink: 0;
text-decoration: none;
}
.activity-commit-sha:hover { text-decoration: underline; }
.activity-commit-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.activity-time {
flex-shrink: 0;
font-size: 0.78rem;
color: #484f58;
margin-left: auto;
padding-left: 12px;
white-space: nowrap;
}
custom-landing.js: |
(async function () {
const grid = document.getElementById('repo-grid');
if (!grid) return;
const baseUrl = window.GITEA_SUB_URL || '';
function 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';
}
function escapeHtml(str) {
return (str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function langDot(lang) {
const colors = {
Go:'#00ADD8', Python:'#3572A5', JavaScript:'#f1e05a', TypeScript:'#2b7489',
Rust:'#dea584', Java:'#b07219', 'C#':'#178600', Nix:'#7e7eff',
Shell:'#89e051', HTML:'#e34c26', CSS:'#563d7c', Elixir:'#6e4a7e',
};
return colors[lang]
? `<span style="width:10px;height:10px;border-radius:50%;background:${colors[lang]};display:inline-block;flex-shrink:0"></span>`
: '📦';
}
async function fetchJson(url) {
const resp = await fetch(url, { credentials: 'include' });
if (!resp.ok) throw new Error(resp.status);
return resp.json();
}
async function getLatestCommit(repo) {
try {
const commits = await fetchJson(
`${baseUrl}/api/v1/repos/${encodeURIComponent(repo.full_name)}/commits?limit=1&page=1`
);
if (commits && commits.length > 0) return commits[0];
} catch (_) {}
return null;
}
async function loadRepos() {
let repos;
try {
const resp = await fetch(`${baseUrl}/api/v1/repos/search?sort=updated&order=desc&limit=12`, {
credentials: 'include',
});
if (!resp.ok) {
const msg = resp.status === 401 || resp.status === 403
? `Sign in to see repositories (HTTP ${resp.status})`
: `API error: HTTP ${resp.status}`;
grid.innerHTML = `<div class="error-msg">${msg}. <a href="${baseUrl}/explore/repos" style="color:#58a6ff">Browse manually →</a></div>`;
return;
}
const json = await resp.json();
repos = json.data || json;
} catch (e) {
console.error('Gitea landing: repo fetch failed', e);
grid.innerHTML = `<div class="error-msg">
Could not load repositories (${e.message}). <a href="${baseUrl}/explore/repos" style="color:#58a6ff">Browse manually →</a>
</div>`;
return;
}
if (!repos || repos.length === 0) {
grid.innerHTML = `<div class="error-msg">No public repositories found.</div>`;
return;
}
// Sort by updated desc (API may already do this)
repos.sort((a, b) => new Date(b.updated) - new Date(a.updated));
// Render cards immediately, then fill in commits async
grid.innerHTML = '';
for (const repo of repos) {
const card = document.createElement('a');
card.className = 'repo-card';
card.href = `${baseUrl}/${escapeHtml(repo.full_name)}`;
card.dataset.repoName = repo.full_name;
card.innerHTML = `
<div class="repo-card-header">
<span class="repo-icon">${langDot(repo.language)}</span>
<span class="repo-name">${escapeHtml(repo.name)}</span>
${repo.private ? '<span class="repo-private">private</span>' : ''}
</div>
<div class="repo-desc">${escapeHtml(repo.description) || '<em style="color:#484f58">No description</em>'}</div>
<div class="repo-meta">
${repo.language ? `<span>${langDot(repo.language)} ${escapeHtml(repo.language)}</span>` : ''}
<span>⭐ ${repo.stars_count || 0}</span>
<span>🍴 ${repo.forks_count || 0}</span>
<span>🕒 ${timeAgo(repo.updated)}</span>
</div>
<div class="repo-commit" id="commit-${CSS.escape(repo.full_name)}">
<span class="commit-dot"></span>
<span class="commit-msg" style="color:#484f58">loading commit…</span>
</div>
`.trim();
grid.appendChild(card);
}
// Fetch commits in parallel
await Promise.all(repos.map(async (repo) => {
const commit = await getLatestCommit(repo);
const el = document.getElementById(`commit-${CSS.escape(repo.full_name)}`);
if (!el) return;
if (commit) {
const msg = commit.commit?.message?.split('\n')[0] || '';
const when = timeAgo(commit.commit?.committer?.date || commit.created);
el.innerHTML = `
<span class="commit-dot"></span>
<span class="commit-msg">${escapeHtml(msg)}</span>
<span class="commit-time">${when}</span>
`;
} else {
el.innerHTML = `<span class="commit-dot" style="background:#484f58"></span><span class="commit-msg" style="color:#484f58">no commits visible</span>`;
}
}));
}
loadRepos();
})();
(async function loadActivity() {
const feed = document.getElementById('activity-feed');
if (!feed) return;
const baseUrl = window.GITEA_SUB_URL || '';
function 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';
}
function esc(str) {
return (str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
let doc;
try {
const resp = await fetch(`${baseUrl}/alex.rss`);
if (!resp.ok) {
feed.innerHTML = `<div style="padding:24px;text-align:center;color:#8b949e">Activity unavailable (HTTP ${resp.status})</div>`;
return;
}
const text = await resp.text();
doc = new DOMParser().parseFromString(text, 'application/xml');
} catch (e) {
console.error('activity rss error', e);
feed.innerHTML = `<div style="padding:24px;text-align:center;color:#8b949e">Could not load activity</div>`;
return;
}
const items = Array.from(doc.querySelectorAll('channel > item')).slice(0, 20);
if (items.length === 0) {
feed.innerHTML = `<div style="padding:24px;text-align:center;color:#8b949e">No public activity yet.</div>`;
return;
}
feed.innerHTML = '';
for (const item of items) {
const title = item.querySelector('title')?.textContent || '';
const link = item.querySelector('link')?.textContent || '#';
const pubDate = item.querySelector('pubDate')?.textContent || '';
const description = item.querySelector('description')?.textContent || '';
const when = pubDate ? timeAgo(pubDate) : '';
// Guess icon from title
let icon = '⚡';
const t = title.toLowerCase();
if (t.includes('push') || t.includes('commit')) icon = '📤';
else if (t.includes('creat') && t.includes('repo')) icon = '📁';
else if (t.includes('fork')) icon = '🍴';
else if (t.includes('open') && t.includes('issue')) icon = '🔴';
else if (t.includes('clos') && t.includes('issue')) icon = '🟢';
else if (t.includes('pull request') || t.includes('merge')) icon = '🔀';
else if (t.includes('tag')) icon = '🏷️';
else if (t.includes('branch')) icon = '🌿';
else if (t.includes('comment')) icon = '💬';
else if (t.includes('release')) icon = '🚀';
// Extract commit lines from description HTML
let commitsHtml = '';
const descDoc = new DOMParser().parseFromString(description, 'text/html');
const commitEls = descDoc.querySelectorAll('li');
if (commitEls.length > 0) {
commitsHtml = '<div class="activity-commits">' +
Array.from(commitEls).slice(0, 3).map(li => {
const anchor = li.querySelector('a');
const sha = anchor ? esc(anchor.textContent.trim().slice(0, 7)) : '';
const shaHref = anchor ? esc(anchor.getAttribute('href') || '#') : '#';
const msg = esc(li.textContent.replace(anchor?.textContent || '', '').trim().replace(/^[-:]\s*/, ''));
return `<div class="activity-commit-line">
${sha ? `<a class="activity-commit-sha" href="${shaHref}">${sha}</a>` : ''}
<span class="activity-commit-text">${msg}</span>
</div>`;
}).join('') +
(commitEls.length > 3 ? `<div class="activity-commit-line" style="color:#484f58">+${commitEls.length - 3} more</div>` : '') +
'</div>';
}
const el = document.createElement('div');
el.className = 'activity-item';
el.innerHTML = `
<div class="activity-op-icon">${icon}</div>
<div class="activity-body">
<div class="activity-headline"><a href="${esc(link)}">${esc(title)}</a></div>
${commitsHtml}
</div>
<span class="activity-time">${when}</span>
`;
feed.appendChild(el);
}
})();