feat(ui): contribution graph with daily activity heatmap
Add /v1/activity/daily endpoint returning per-day event counts via generate_series + LEFT JOIN. Frontend renders an SVG contribution graph with circles colored by quantile-based thresholds. Clicking a day navigates to /activity/YYYY-MM-DD showing that day's events. New /activity/:timespan route parses single dates (YYYY-MM-DD) and ranges (YYYY-MM-DD..YYYY-MM-DD) from the URL to initialize the activity timeline filter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
@@ -11,7 +12,24 @@ import { TimelineEntry } from '../components/TimelineEntry';
|
||||
const RANGE_MIN = new Date('2010-01-01T00:00:00Z').getTime();
|
||||
const RANGE_MAX = Date.now();
|
||||
|
||||
function parseTimespan(timespan?: string): [number, number] | null {
|
||||
if (!timespan) return null;
|
||||
if (timespan.includes('..')) {
|
||||
const [a, b] = timespan.split('..');
|
||||
const from = new Date(a + 'T00:00:00Z').getTime();
|
||||
const to = new Date(b + 'T23:59:59Z').getTime();
|
||||
if (!isNaN(from) && !isNaN(to)) return [from, to];
|
||||
} else {
|
||||
const from = new Date(timespan + 'T00:00:00Z').getTime();
|
||||
const to = new Date(timespan + 'T23:59:59Z').getTime();
|
||||
if (!isNaN(from)) return [from, to];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function TimelineHome() {
|
||||
const { timespan } = useParams();
|
||||
|
||||
const [enabledSources, setEnabledSources] = useState<Record<Source, boolean>>({
|
||||
github: true,
|
||||
gitea: true,
|
||||
@@ -19,6 +37,8 @@ export function TimelineHome() {
|
||||
bugzilla: true,
|
||||
});
|
||||
const [rangeValue, setRangeValue] = useState<[number, number]>(() => {
|
||||
const parsed = parseTimespan(timespan);
|
||||
if (parsed) return parsed;
|
||||
const now = Date.now();
|
||||
const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
|
||||
return [thirtyDaysAgo, now];
|
||||
|
||||
Reference in New Issue
Block a user