From bf986529b39b0b8eeec5f503e741dbd18260e356 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 22 May 2026 20:55:02 -0500 Subject: [PATCH] feat(heatmap): add range selection component and update range keys for activity metrics --- .../src/components/git/ForgejoHeatmap.tsx | 104 +++++++++++++----- .../git/ForgejoIssueMetricCards.tsx | 21 +++- 2 files changed, 92 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/git/ForgejoHeatmap.tsx b/frontend/src/components/git/ForgejoHeatmap.tsx index b961e5e..7c15ed9 100644 --- a/frontend/src/components/git/ForgejoHeatmap.tsx +++ b/frontend/src/components/git/ForgejoHeatmap.tsx @@ -3,6 +3,13 @@ import { useMemo, useState } from "react"; import { Activity, LayoutGrid } from "lucide-react"; import type { ForgejoHeatmapDay, ForgejoLastPush } from "@/lib/api-forgejo"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; interface ForgejoHeatmapProps { days: ForgejoHeatmapDay[]; @@ -30,7 +37,7 @@ const HRIGHT = 14; const HVH = LVH; // ── Types & data ─────────────────────────────────────────────────────────── -type RangeKey = "7d" | "30d" | "3m" | "6m" | "1y"; +type RangeKey = "7d" | "14d" | "30d" | "90d" | "6m" | "1y"; type ActivityDatum = { date: string; count: number; @@ -38,16 +45,17 @@ type ActivityDatum = { }; const RANGE_DAYS: Record = { - "7d": 7, "30d": 30, "3m": 91, "6m": 183, "1y": 365, + "7d": 7, "14d": 14, "30d": 30, "90d": 90, "6m": 183, "1y": 365, }; const RANGE_SUMMARY: Record = { "7d": "7 days", + "14d": "14 days", "30d": "30 days", - "3m": "3 months", + "90d": "90 days", "6m": "6 months", "1y": "1 year", }; -const RANGE_LABELS: RangeKey[] = ["7d", "30d", "3m", "6m", "1y"]; +const RANGE_LABELS: RangeKey[] = ["7d", "14d", "30d", "90d", "6m", "1y"]; const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; // ── Colors ───────────────────────────────────────────────────────────────── @@ -111,6 +119,53 @@ function heatFill(count: number, maxCount: number): string { return `rgba(16,185,129,${alpha.toFixed(2)})`; } +function RangeSelect({ + value, + onValueChange, + accent, + ariaLabel, +}: { + value: RangeKey; + onValueChange: (value: RangeKey) => void; + accent: "violet" | "green"; + ariaLabel: string; +}) { + const isViolet = accent === "violet"; + return ( + + ); +} + // ── Line chart sub-component ─────────────────────────────────────────────── function LineChart({ days, range, onRangeChange, lastPush }: { days: ForgejoHeatmapDay[]; @@ -206,18 +261,12 @@ function LineChart({ days, range, onRangeChange, lastPush }: { {hoveredPoint ? `on ${displayedLabel}` : `in ${displayedLabel}`} -
- {RANGE_LABELS.map(r => ( - - ))} -
+ {/* SVG */} @@ -389,7 +438,7 @@ function HeatmapGrid({ days, range, onRangeChange }: { const gap = 3; const availableWidth = LVW - HLEFT - HRIGHT; - const maxCell = range === "3m" ? 14 : range === "6m" ? 12 : 8; + const maxCell = range === "90d" ? 14 : range === "6m" ? 12 : 8; const cell = Math.max( 6, Math.min(maxCell, Math.floor((availableWidth - gap * (weekCount - 1)) / weekCount)), @@ -430,17 +479,12 @@ function HeatmapGrid({ days, range, onRangeChange }: { {hoveredDay ? `on ${displayedLabel}` : `in ${displayedLabel}`} -
- {RANGE_LABELS.map(r => ( - - ))} -
+ {/* SVG */} @@ -543,8 +587,8 @@ export function ForgejoHeatmap({ lastPush = null, isLoading = false, }: ForgejoHeatmapProps) { - const [lineRange, setLineRange] = useState("7d"); - const [heatRange, setHeatRange] = useState("7d"); + const [lineRange, setLineRange] = useState("14d"); + const [heatRange, setHeatRange] = useState("14d"); if (isLoading) { return ( diff --git a/frontend/src/components/git/ForgejoIssueMetricCards.tsx b/frontend/src/components/git/ForgejoIssueMetricCards.tsx index 569ec30..b8e7126 100644 --- a/frontend/src/components/git/ForgejoIssueMetricCards.tsx +++ b/frontend/src/components/git/ForgejoIssueMetricCards.tsx @@ -43,7 +43,8 @@ const formatCount = (value: number | null | undefined): string => const parseDate = (value: string | null | undefined): Date | null => { if (!value) return null; - const date = new Date(value); + const hasTimezone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(value); + const date = new Date(hasTimezone ? value : `${value}Z`); return Number.isNaN(date.getTime()) ? null : date; }; @@ -183,6 +184,7 @@ function MetricSkeleton() { // ── Card renderer ────────────────────────────────────────────────────────── function MetricCardLink({ card }: { card: MetricCard }) { const Icon = card.icon; + const valueParts = card.value.split(/(\d[\d,]*)/g); return ( {card.title}

-

- {card.value} +

+ {valueParts.map((part, index) => + /^\d[\d,]*$/.test(part) ? ( + + {part} + + ) : ( + part + ), + )}