162 lines
4.6 KiB
TypeScript
162 lines
4.6 KiB
TypeScript
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 (
|
|
<>
|
|
<Row className="mb-3">
|
|
<Col>
|
|
<h2><a href={repoUrl(source ?? '', host, repo)} target="_blank" rel="noopener noreferrer"><img src={forgeIcon(source ?? '')} alt={source} className="forge-icon" style={{ width: 24, height: 24 }} /></a>{repo}</h2>
|
|
{langs && <LanguageBar languages={langs} />}
|
|
</Col>
|
|
</Row>
|
|
|
|
{readmeQ.data && (
|
|
<Row className="mb-4">
|
|
<Col>
|
|
<div className="project-readme">
|
|
<ReactMarkdown>{readmeQ.data}</ReactMarkdown>
|
|
</div>
|
|
</Col>
|
|
</Row>
|
|
)}
|
|
|
|
<Row>
|
|
<Col>
|
|
<p style={{ fontSize: '85%' }}>
|
|
{eventsQ.isLoading
|
|
? 'loading...'
|
|
: eventsQ.isError
|
|
? `error: ${(eventsQ.error as Error).message}`
|
|
: `${events.length} ${events.length === 1 ? 'activity' : 'activities'}`}
|
|
</p>
|
|
<VerticalTimeline>
|
|
{events.map((item) => (
|
|
<TimelineEntry key={item.id} item={item} />
|
|
))}
|
|
</VerticalTimeline>
|
|
</Col>
|
|
</Row>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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<string, number> }) {
|
|
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 (
|
|
<div className="mt-2">
|
|
<div className="language-bar">
|
|
{sorted.map(([lang, bytes]) => (
|
|
<div
|
|
key={lang}
|
|
className="language-bar-segment"
|
|
style={{ width: `${(bytes / total) * 100}%`, backgroundColor: langColor(lang) }}
|
|
title={`${lang} ${((bytes / total) * 100).toFixed(1)}%`}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="d-flex flex-wrap gap-2 mt-1" style={{ fontSize: '0.75rem' }}>
|
|
{sorted.slice(0, 8).map(([lang, bytes]) => (
|
|
<span key={lang}>
|
|
<span className="language-dot" style={{ backgroundColor: langColor(lang) }} />
|
|
{lang} {((bytes / total) * 100).toFixed(1)}%
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const LANG_COLORS: Record<string, string> = {
|
|
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';
|
|
}
|