'use client'; import { useEffect, useMemo, useState } from 'react'; import { getRepos } from '@/lib/gitea'; import type { GiteaRepo } from '@/lib/types'; import { GITEA_URL, GITEA_USERNAME } from '@/lib/config'; const LANG_COLOR: Record = { python: '#3572A5', javascript: '#f1e05a', typescript: '#3178c6', go: '#00add8', rust: '#dea584', 'c++': '#f34b7d', c: '#555', html: '#e34c26', css: '#563d7c', shell: '#89e051', ruby: '#701516', java: '#b07219', }; function langColor(l: string | null) { return LANG_COLOR[(l || '').toLowerCase()] || '#555'; } function timeAgo(d: string | undefined | null) { if (!d) return ''; const t = new Date(d).getTime(); if (Number.isNaN(t)) return ''; const diff = Date.now() - t; if (diff < 0) return 'just now'; const m = Math.floor(diff / 60000); if (m < 1) return 'just now'; if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; const days = Math.floor(h / 24); if (days < 30) return `${days}d ago`; const months = Math.floor(days / 30); return `${months}mo ago`; } type SortKey = 'recent' | 'stars'; export default function Projects() { const [repos, setRepos] = useState(null); const [error, setError] = useState(null); const [activeRepo, setActiveRepo] = useState(null); const [readmeHtml, setReadmeHtml] = useState(null); const [readmeLoading, setReadmeLoading] = useState(false); const [readmeError, setReadmeError] = useState(null); const [sortBy, setSortBy] = useState('recent'); const [languageFilter, setLanguageFilter] = useState('all'); useEffect(() => { // Get all repos (or a high limit) instead of SITE.repoLimit getRepos(100) .then(setRepos) .catch(e => setError(e instanceof Error ? e.message : String(e))); }, []); // Load README when a repo is opened useEffect(() => { if (!activeRepo) return; setReadmeLoading(true); setReadmeError(null); setReadmeHtml(null); fetch(`/api/gitea/readme?repo=${encodeURIComponent(activeRepo.name)}`) .then(res => { if (!res.ok) throw new Error(`README HTTP ${res.status}`); return res.text(); }) .then(html => setReadmeHtml(html)) .catch(e => setReadmeError( e instanceof Error ? e.message : 'Could not load README', ), ) .finally(() => setReadmeLoading(false)); }, [activeRepo]); // Derive list of languages for filter const languages = useMemo(() => { if (!repos) return []; const set = new Set(); repos.forEach(r => { if (r.language) set.add(r.language); }); return Array.from(set).sort((a, b) => a.localeCompare(b)); }, [repos]); // Apply sort + language filter const filteredRepos = useMemo(() => { if (!repos) return null; let list = [...repos]; if (languageFilter !== 'all') { list = list.filter( r => (r.language || '').toLowerCase() === languageFilter.toLowerCase(), ); } list.sort((a, b) => { if (sortBy === 'stars') { return b.stars_count - a.stars_count; } // recent: sort by updated/updated_at desc const ad = new Date((a.updated || (a as any).updated_at) ?? '').getTime(); const bd = new Date((b.updated || (b as any).updated_at) ?? '').getTime(); return bd - ad; }); return list; }, [repos, sortBy, languageFilter]); return (

Projects

Open-source work, sorted by activity or stars.

{/* Sort */}
Sort:
{/* Language filter */}
Language:
{/* All repos link */} All repos
{/* Loading */} {!filteredRepos && !error && (
{Array.from({ length: 6 }).map((_, i) => (
))}
)} {/* Error */} {error && (

Could not load repositories

{error}

)} {/* Repos list */} {filteredRepos && (
{filteredRepos.map(repo => { const updated = repo.updated || (repo as any).updated_at; return ( ); })}
)} {/* Popout with README + Live + Wiki (unchanged) */} {activeRepo && (

{activeRepo.name}

{activeRepo.description || 'This project does not have a description yet.'}

View code {activeRepo.website && activeRepo.website.trim().length > 0 && ( View live )} Wiki
{readmeLoading && (

Loading README…

)} {readmeError && (

Could not load README: {readmeError}

)} {readmeHtml && (
)} {!readmeLoading && !readmeError && !readmeHtml && (

No README content available for this repository.

)}
)}
); }