feat(ui): project readme, language bars, and per-card language summary
ProjectPage fetches README (raw markdown) and language breakdown from GitHub/Gitea REST APIs, rendering the readme as markdown and languages as a colored proportional bar with labels. Dashboard cards lazily fetch top 3 languages per repo and display them inline. Language color map covers common languages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,15 +2,26 @@ 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, type Source } from '../api/client';
|
||||
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: () =>
|
||||
@@ -22,7 +33,22 @@ export function ProjectPage() {
|
||||
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 (
|
||||
<>
|
||||
@@ -30,8 +56,20 @@ export function ProjectPage() {
|
||||
<Col>
|
||||
<h2>{repo}</h2>
|
||||
<small className="text-muted">{source}</small>
|
||||
{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%' }}>
|
||||
@@ -51,3 +89,56 @@ export function ProjectPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user