feat(ui): add /dash route, shared nav, project dashboard with /v1/projects API
Restructure routes: / and /dash show a project overview dashboard, /activity hosts the existing timeline, /cv remains. Shared Layout component provides consistent nav header and footer across all routes. New /v1/projects endpoint aggregates per-repo activity stats (commits, issues, PRs, date range) from existing event data via SQL. Dashboard ranks projects by weighted recency + volume score and renders a card grid. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
ui/src/pages/DashPage.tsx
Normal file
102
ui/src/pages/DashPage.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
|
||||
import { fetchProjects, type ProjectSummary } from '../api/client';
|
||||
|
||||
export function DashPage() {
|
||||
const projectsQ = useQuery({
|
||||
queryKey: ['projects'],
|
||||
queryFn: fetchProjects,
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
|
||||
const projects = projectsQ.data ?? [];
|
||||
const ranked = rankProjects(projects).slice(0, 24);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row className="mb-3">
|
||||
<Col>
|
||||
<p>
|
||||
i rarely say anything that warrants capital letters. a peek into the
|
||||
projects i'm working on is below.
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
{projectsQ.isLoading && <p>loading...</p>}
|
||||
{projectsQ.isError && (
|
||||
<p>error: {(projectsQ.error as Error).message}</p>
|
||||
)}
|
||||
<Row xs={1} md={2} lg={3} className="g-3">
|
||||
{ranked.map((p) => (
|
||||
<Col key={`${p.source}:${p.repo}`}>
|
||||
<div className="project-card p-3">
|
||||
<h5 className="mb-1">
|
||||
<a
|
||||
href={repoUrl(p)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{p.repo}
|
||||
</a>
|
||||
</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>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function repoUrl(p: ProjectSummary): string {
|
||||
switch (p.source) {
|
||||
case 'github':
|
||||
return `https://github.com/${p.repo}`;
|
||||
case 'gitea':
|
||||
return `https://${p.host}/${p.repo}`;
|
||||
case 'hg':
|
||||
return `https://${p.host}/${p.repo}`;
|
||||
case 'bugzilla':
|
||||
return `https://${p.host}`;
|
||||
default:
|
||||
return '#';
|
||||
}
|
||||
}
|
||||
|
||||
function formatRange(first: string | null, last: string | null): string {
|
||||
const fmt = (iso: string) =>
|
||||
new Date(iso).toLocaleDateString('en-GB', { month: 'short', year: 'numeric' }).toLowerCase();
|
||||
if (first && last) return `${fmt(first)} — ${fmt(last)}`;
|
||||
if (last) return fmt(last);
|
||||
return '';
|
||||
}
|
||||
|
||||
function rankProjects(projects: ProjectSummary[]): ProjectSummary[] {
|
||||
if (projects.length === 0) return [];
|
||||
const now = Date.now();
|
||||
const maxVolume = Math.max(...projects.map((p) => p.commit_count + p.issue_count + p.pr_count));
|
||||
const oldest = Math.min(
|
||||
...projects.map((p) => (p.last_activity ? new Date(p.last_activity).getTime() : 0)),
|
||||
);
|
||||
const range = now - oldest || 1;
|
||||
|
||||
return [...projects].sort((a, b) => score(b) - score(a));
|
||||
|
||||
function score(p: ProjectSummary): number {
|
||||
const volume = (p.commit_count + p.issue_count + p.pr_count) / (maxVolume || 1);
|
||||
const recency = p.last_activity
|
||||
? (new Date(p.last_activity).getTime() - oldest) / range
|
||||
: 0;
|
||||
return 0.6 * recency + 0.4 * volume;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user