apiVersion: v1 kind: ConfigMap metadata: name: gitea-landing-page namespace: gitea data: home.tmpl: | {{template "base/head" .}}

Alex Mickelson

Recent Projects

Latest commits across all repositories

Recent Activity

What Alex has been up to View full profile β†’
{{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,'&').replace(//g,'>').replace(/"/g,'"'); } 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] ? `` : 'πŸ“¦'; } 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 = `
${msg}. Browse manually β†’
`; return; } const json = await resp.json(); repos = json.data || json; } catch (e) { console.error('Gitea landing: repo fetch failed', e); grid.innerHTML = `
Could not load repositories (${e.message}). Browse manually β†’
`; return; } if (!repos || repos.length === 0) { grid.innerHTML = `
No public repositories found.
`; 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 = `
${langDot(repo.language)} ${escapeHtml(repo.name)} ${repo.private ? 'private' : ''}
${escapeHtml(repo.description) || 'No description'}
${repo.language ? `${langDot(repo.language)} ${escapeHtml(repo.language)}` : ''} ⭐ ${repo.stars_count || 0} 🍴 ${repo.forks_count || 0} πŸ•’ ${timeAgo(repo.updated)}
loading commit…
`.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 = ` ${escapeHtml(msg)} ${when} `; } else { el.innerHTML = `no commits visible`; } })); } 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,'&').replace(//g,'>').replace(/"/g,'"'); } let doc; try { const resp = await fetch(`${baseUrl}/alex.rss`); if (!resp.ok) { feed.innerHTML = `
Activity unavailable (HTTP ${resp.status})
`; return; } const text = await resp.text(); doc = new DOMParser().parseFromString(text, 'application/xml'); } catch (e) { console.error('activity rss error', e); feed.innerHTML = `
Could not load activity
`; return; } const items = Array.from(doc.querySelectorAll('channel > item')).slice(0, 20); if (items.length === 0) { feed.innerHTML = `
No public activity yet.
`; 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 = '
' + 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 `
${sha ? `${sha}` : ''} ${msg}
`; }).join('') + (commitEls.length > 3 ? `
+${commitEls.length - 3} more
` : '') + '
'; } const el = document.createElement('div'); el.className = 'activity-item'; el.innerHTML = `
${icon}
${esc(title)}
${commitsHtml}
${when} `; feed.appendChild(el); } })();