Files
moments/ui/src/pages/TimelineHome.tsx
rob thijssen a70fab4feb 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>
2026-05-05 15:19:49 +03:00

89 lines
2.5 KiB
TypeScript

import { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import Col from 'react-bootstrap/Col';
import Row from 'react-bootstrap/Row';
import { VerticalTimeline } from 'react-vertical-timeline-component';
import { fetchEvents, fetchSources, type Source } from '../api/client';
import { Filters } from '../components/Filters';
import { TimelineEntry } from '../components/TimelineEntry';
const RANGE_MIN = new Date('2010-01-01T00:00:00Z').getTime();
const RANGE_MAX = Date.now();
export function TimelineHome() {
const [enabledSources, setEnabledSources] = useState<Record<Source, boolean>>({
github: true,
gitea: true,
hg: true,
bugzilla: true,
});
const [rangeValue, setRangeValue] = useState<[number, number]>(() => {
const now = Date.now();
const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
return [thirtyDaysAgo, now];
});
const [limit, setLimit] = useState<number>(100);
const sourcesQ = useQuery({
queryKey: ['sources'],
queryFn: fetchSources,
refetchInterval: 60_000,
});
const activeSources = useMemo(
() =>
(Object.keys(enabledSources) as Source[]).filter((s) => enabledSources[s]),
[enabledSources],
);
const eventsQ = useQuery({
queryKey: ['events', rangeValue, activeSources, limit],
queryFn: () =>
fetchEvents({
from: new Date(rangeValue[0]),
to: new Date(rangeValue[1]),
sources: activeSources,
limit,
}),
refetchInterval: 60_000,
});
const events = eventsQ.data ?? [];
return (
<>
<Filters
enabledSources={enabledSources}
onSourceToggle={(s, on) =>
setEnabledSources((prev) => ({ ...prev, [s]: on }))
}
rangeMin={RANGE_MIN}
rangeMax={RANGE_MAX}
rangeValue={rangeValue}
onRangeChange={setRangeValue}
limit={limit}
onLimitChange={setLimit}
summaries={sourcesQ.data}
/>
<Row>
<Col>
<p className="text-center" style={{ fontSize: '85%' }}>
{eventsQ.isLoading
? 'loading…'
: eventsQ.isError
? `error: ${(eventsQ.error as Error).message}`
: `showing ${events.length} ${events.length === 1 ? 'activity' : 'activities'}`}
</p>
<VerticalTimeline>
{events.map((item) => (
<TimelineEntry key={item.id} item={item} />
))}
</VerticalTimeline>
</Col>
</Row>
</>
);
}