import { useParams } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import Col from 'react-bootstrap/Col'; import Row from 'react-bootstrap/Row'; import ReactMarkdown from 'react-markdown'; import { VerticalTimeline } from 'react-vertical-timeline-component'; import { fetchEvents, fetchReadme, fetchLanguages, fetchProjects, type Source } from '../api/client'; import { TimelineEntry } from '../components/TimelineEntry'; export function ProjectPage() { const { source, '*': repoPath } = useParams(); const repo = repoPath ?? ''; const projectsQ = useQuery({ queryKey: ['projects'], queryFn: fetchProjects, staleTime: 60_000, }); const project = projectsQ.data?.find( (p) => p.source === source && p.repo === repo, ); const host = project?.host ?? ''; const eventsQ = useQuery({ queryKey: ['project-events', source, repo], queryFn: () => fetchEvents({ sources: source ? [source as Source] : undefined, repo, limit: 500, }), refetchInterval: 60_000, }); const readmeQ = useQuery({ queryKey: ['readme', source, host, repo], queryFn: () => fetchReadme(source as Source, host, repo), enabled: !!host && (source === 'github' || source === 'gitea'), staleTime: 5 * 60_000, }); const langsQ = useQuery({ queryKey: ['languages', source, host, repo], queryFn: () => fetchLanguages(source as Source, host, repo), enabled: !!host && (source === 'github' || source === 'gitea'), staleTime: 5 * 60_000, }); const events = eventsQ.data ?? []; const langs = langsQ.data; return ( <> {repo} {langs && } {readmeQ.data && ( {readmeQ.data} )} {eventsQ.isLoading ? 'loading...' : eventsQ.isError ? `error: ${(eventsQ.error as Error).message}` : `${events.length} ${events.length === 1 ? 'activity' : 'activities'}`} {events.map((item) => ( ))} > ); } function repoUrl(source: string, host: string, repo: string): string { switch (source) { case 'github': return `https://github.com/${repo}`; case 'gitea': return `https://${host}/${repo}`; case 'hg': return `https://${host}/${repo}`; default: return '#'; } } function forgeIcon(source: string): string { switch (source) { case 'github': return '/github.svg'; case 'gitea': return '/gitea.svg'; case 'hg': return '/mozilla.svg'; default: return '/github.svg'; } } function LanguageBar({ languages }: { languages: Record }) { const total = Object.values(languages).reduce((a, b) => a + b, 0); if (total === 0) return null; const sorted = Object.entries(languages).sort(([, a], [, b]) => b - a); return ( {sorted.map(([lang, bytes]) => ( ))} {sorted.slice(0, 8).map(([lang, bytes]) => ( {lang} {((bytes / total) * 100).toFixed(1)}% ))} ); } const LANG_COLORS: Record = { Rust: '#dea584', TypeScript: '#3178c6', JavaScript: '#f1e05a', Python: '#3572a5', Go: '#00add8', Shell: '#89e051', HTML: '#e34c26', CSS: '#563d7c', C: '#555555', 'C++': '#f34b7d', Java: '#b07219', Ruby: '#701516', Nix: '#7e7eff', Makefile: '#427819', Dockerfile: '#384d54', SCSS: '#c6538c', }; function langColor(lang: string): string { return LANG_COLORS[lang] ?? '#8b8b8b'; }
{eventsQ.isLoading ? 'loading...' : eventsQ.isError ? `error: ${(eventsQ.error as Error).message}` : `${events.length} ${events.length === 1 ? 'activity' : 'activities'}`}