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
+ ),
+ )}