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:
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
|
||||
import { fetchProjects, type ProjectSummary } from '../api/client';
|
||||
import { fetchProjects, fetchLanguages, type ProjectSummary, type Source } from '../api/client';
|
||||
|
||||
export function DashPage() {
|
||||
const projectsQ = useQuery({
|
||||
@@ -32,20 +32,7 @@ export function DashPage() {
|
||||
<Row xs={1} md={2} lg={3} className="g-3">
|
||||
{ranked.map((p) => (
|
||||
<Col key={`${p.source}:${p.repo}`}>
|
||||
<Link to={`/project/${p.source}/${p.repo}`} className="text-decoration-none">
|
||||
<div className="project-card p-3">
|
||||
<h5 className="mb-1">{p.repo}</h5>
|
||||
<small className="text-muted d-block mb-2">{p.source}</small>
|
||||
<div className="d-flex gap-3" style={{ fontSize: '0.8rem' }}>
|
||||
{p.commit_count > 0 && <span>{p.commit_count} commits</span>}
|
||||
{p.issue_count > 0 && <span>{p.issue_count} issues</span>}
|
||||
{p.pr_count > 0 && <span>{p.pr_count} prs</span>}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', opacity: 0.6, marginTop: '0.25rem' }}>
|
||||
{formatRange(p.first_activity, p.last_activity)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<ProjectCard project={p} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
@@ -53,6 +40,46 @@ export function DashPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectCard({ project: p }: { project: ProjectSummary }) {
|
||||
const langsQ = useQuery({
|
||||
queryKey: ['languages', p.source, p.host, p.repo],
|
||||
queryFn: () => fetchLanguages(p.source as Source, p.host, p.repo),
|
||||
enabled: p.source === 'github' || p.source === 'gitea',
|
||||
staleTime: 10 * 60_000,
|
||||
});
|
||||
|
||||
const langs = langsQ.data;
|
||||
const topLangs = langs ? topLanguages(langs, 3) : null;
|
||||
|
||||
return (
|
||||
<Link to={`/project/${p.source}/${p.repo}`} className="text-decoration-none">
|
||||
<div className="project-card p-3">
|
||||
<h5 className="mb-1">{p.repo}</h5>
|
||||
<small className="text-muted d-block mb-2">
|
||||
{p.source}
|
||||
{topLangs && ` · ${topLangs}`}
|
||||
</small>
|
||||
<div className="d-flex gap-3" style={{ fontSize: '0.8rem' }}>
|
||||
{p.commit_count > 0 && <span>{p.commit_count} commits</span>}
|
||||
{p.issue_count > 0 && <span>{p.issue_count} issues</span>}
|
||||
{p.pr_count > 0 && <span>{p.pr_count} prs</span>}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', opacity: 0.6, marginTop: '0.25rem' }}>
|
||||
{formatRange(p.first_activity, p.last_activity)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function topLanguages(langs: Record<string, number>, n: number): string {
|
||||
return Object.entries(langs)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, n)
|
||||
.map(([lang]) => lang.toLowerCase())
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function formatRange(first: string | null, last: string | null): string {
|
||||
const fmt = (iso: string) =>
|
||||
new Date(iso).toLocaleDateString('en-GB', { month: 'short', year: 'numeric' }).toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user