feat(navbar): enhance reset label formatting to include full reset date
This commit is contained in:
parent
fded1d16da
commit
b01c11fb05
|
|
@ -97,10 +97,11 @@ function toLevel(count: number): number {
|
|||
}
|
||||
|
||||
// ── Line chart sub-component ───────────────────────────────────────────────
|
||||
function LineChart({ days, range, onRangeChange }: {
|
||||
function LineChart({ days, range, onRangeChange, lastPush }: {
|
||||
days: ForgejoHeatmapDay[];
|
||||
range: RangeKey;
|
||||
onRangeChange: (r: RangeKey) => void;
|
||||
lastPush: ForgejoLastPush | null;
|
||||
}) {
|
||||
const { points, xLabels, yLabels, linePath, areaPath } = useMemo(() => {
|
||||
const lookup = new Map(days.map(d => [d.date, d.count]));
|
||||
|
|
@ -218,13 +219,37 @@ function LineChart({ days, range, onRangeChange }: {
|
|||
</rect>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
{/* Last push — centered at bottom of card */}
|
||||
{lastPush && (
|
||||
<div className="mt-3 flex items-start justify-center gap-2.5">
|
||||
<span className="mt-0.5 shrink-0 rounded px-1.5 py-0.5 font-mono text-[11px] font-semibold"
|
||||
style={{background:"rgba(139,92,246,0.20)", color:VIOLET}}>
|
||||
{lastPush.sha}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium" style={{color:"rgba(255,255,255,0.90)"}}>
|
||||
{lastPush.message}
|
||||
</p>
|
||||
<p className="mt-0.5 text-center text-xs" style={{color:"rgba(255,255,255,0.50)"}}>
|
||||
{lastPush.author} pushed to{" "}
|
||||
<span style={{color:"rgba(139,92,246,0.90)"}}>{lastPush.branch}</span>
|
||||
{" · "}{lastPush.repo}{" · "}{fmtRelative(lastPush.pushed_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Heatmap grid sub-component ─────────────────────────────────────────────
|
||||
function HeatmapGrid({ days }: { days: ForgejoHeatmapDay[] }) {
|
||||
const { weeks, monthLabels } = useMemo(() => {
|
||||
function HeatmapGrid({ days, range, onRangeChange }: {
|
||||
days: ForgejoHeatmapDay[];
|
||||
range: RangeKey;
|
||||
onRangeChange: (r: RangeKey) => void;
|
||||
}) {
|
||||
const { weeks, monthLabels, totalEvents } = useMemo(() => {
|
||||
const lookup = new Map(days.map(d => [d.date, d.count]));
|
||||
const today = new Date(); today.setHours(0,0,0,0);
|
||||
const start = new Date(today);
|
||||
|
|
@ -251,51 +276,72 @@ function HeatmapGrid({ days }: { days: ForgejoHeatmapDay[] }) {
|
|||
}
|
||||
builtWeeks.push(week);
|
||||
}
|
||||
return { weeks: builtWeeks, monthLabels: monthLabelList };
|
||||
}, [days]);
|
||||
|
||||
// contributions count for selected range
|
||||
const numDays = RANGE_DAYS[range];
|
||||
const cutoff = new Date(today); cutoff.setDate(cutoff.getDate() - numDays);
|
||||
const cutoffStr = isoDate(cutoff);
|
||||
const totalEvents = days.filter(d => d.date >= cutoffStr).reduce((s,d) => s+d.count, 0);
|
||||
|
||||
return { weeks: builtWeeks, monthLabels: monthLabelList, totalEvents };
|
||||
}, [days, range]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Header */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutGrid className="h-4 w-4" style={{ color: "rgba(16,185,129,1)" }} />
|
||||
<span className="text-sm font-semibold text-strong">Activity Heatmap</span>
|
||||
</div>
|
||||
<span className="rounded px-2 py-0.5 text-xs font-medium"
|
||||
style={{ background:"rgba(16,185,129,0.12)", color:"rgba(16,185,129,0.85)", border:"1px solid rgba(16,185,129,0.25)" }}>
|
||||
Last 12 months
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
{RANGE_LABELS.map(r => (
|
||||
<button key={r} type="button" onClick={() => onRangeChange(r)}
|
||||
className="rounded px-2 py-0.5 text-xs font-medium transition-all"
|
||||
style={r === range
|
||||
? { background:"rgba(16,185,129,0.18)", color:"rgba(16,185,129,1)", border:"1px solid rgba(16,185,129,0.40)" }
|
||||
: { background:"transparent", color:W40, border:"1px solid transparent" }}>
|
||||
{r}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SVG */}
|
||||
<svg viewBox={`0 0 ${HVW} ${HVH}`} width="100%" style={{ display:"block" }} aria-label="Activity heatmap">
|
||||
{/* Month labels */}
|
||||
{monthLabels.map(({weekIdx, label}) => (
|
||||
<text key={`m-${weekIdx}`} x={HLEFT+weekIdx*HSTRIDE} y={11} fontSize={10} style={{fill:W70}}>{label}</text>
|
||||
))}
|
||||
{/* Day labels */}
|
||||
{(["Mon","Wed","Fri"] as const).map((label, i) => (
|
||||
<text key={label} x={0} y={HTOP+(i*2+1)*HSTRIDE+HCELL-2} fontSize={10} style={{fill:W70}}>{label}</text>
|
||||
))}
|
||||
{/* Cells */}
|
||||
{weeks.map((week, wi) =>
|
||||
week.map((cell, di) => cell.future ? null : (
|
||||
<rect key={cell.date}
|
||||
x={HLEFT+wi*HSTRIDE} y={HTOP+di*HSTRIDE}
|
||||
width={HCELL} height={HCELL} rx={2}
|
||||
style={{fill: HMAP_FILL[toLevel(cell.count)]}}>
|
||||
<title>{cell.date}{cell.count>0?`: ${cell.count} event${cell.count!==1?"s":""}`+"" : ": no activity"}</title>
|
||||
<title>{cell.date}{cell.count>0?`: ${cell.count} event${cell.count!==1?"s":""}` : ": no activity"}</title>
|
||||
</rect>
|
||||
))
|
||||
)}
|
||||
{/* Legend */}
|
||||
<text x={HLEFT} y={HVH-4} fontSize={9} style={{fill:W40}}>Less</text>
|
||||
{HMAP_FILL.map((fill, i) => (
|
||||
<rect key={i} x={HLEFT+32+i*(HCELL+3)} y={HVH-HCELL-4} width={HCELL} height={HCELL} rx={2} style={{fill}}/>
|
||||
))}
|
||||
<text x={HLEFT+32+HMAP_FILL.length*(HCELL+3)+2} y={HVH-4} fontSize={9} style={{fill:W40}}>More</text>
|
||||
</svg>
|
||||
|
||||
{/* Contributions summary */}
|
||||
<p className="text-center">
|
||||
<span className="text-2xl font-bold tabular-nums" style={{color:VIOLET}}>
|
||||
{totalEvents.toLocaleString()}
|
||||
</span>
|
||||
<span className="ml-1.5 text-sm" style={{color:W70}}>
|
||||
contributions across all tracked repositories in the last {range}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -309,15 +355,8 @@ export function ForgejoHeatmap({
|
|||
lastPush = null,
|
||||
isLoading = false,
|
||||
}: ForgejoHeatmapProps) {
|
||||
const [lineRange, setLineRange] = useState<RangeKey>("30d");
|
||||
|
||||
const totalEvents = useMemo(() => {
|
||||
const numDays = RANGE_DAYS[lineRange];
|
||||
const today = new Date(); today.setHours(0,0,0,0);
|
||||
const cutoff = new Date(today); cutoff.setDate(cutoff.getDate() - numDays);
|
||||
const cutoffStr = isoDate(cutoff);
|
||||
return days.filter(d => d.date >= cutoffStr).reduce((s,d) => s+d.count, 0);
|
||||
}, [days, lineRange]);
|
||||
const [lineRange, setLineRange] = useState<RangeKey>("7d");
|
||||
const [heatRange, setHeatRange] = useState<RangeKey>("7d");
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -341,43 +380,13 @@ export function ForgejoHeatmap({
|
|||
{/* ── Two cards ───────────────────────────────────────────── */}
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush">
|
||||
<LineChart days={days} range={lineRange} onRangeChange={setLineRange} />
|
||||
<LineChart days={days} range={lineRange} onRangeChange={setLineRange} lastPush={lastPush} />
|
||||
</section>
|
||||
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush">
|
||||
<HeatmapGrid days={days} />
|
||||
<HeatmapGrid days={days} range={heatRange} onRangeChange={setHeatRange} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* ── Contributions summary ────────────────────────────────── */}
|
||||
<p className="text-center">
|
||||
<span className="text-2xl font-bold tabular-nums" style={{color:VIOLET}}>
|
||||
{totalEvents.toLocaleString()}
|
||||
</span>
|
||||
<span className="ml-1.5 text-sm" style={{color:W70}}>
|
||||
contributions across all tracked repositories in the last {lineRange}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{/* ── Last push — no box, plain inline ────────────────────── */}
|
||||
{lastPush && (
|
||||
<div className="mx-auto flex max-w-lg items-start gap-3 px-1">
|
||||
<span className="mt-0.5 shrink-0 rounded px-1.5 py-0.5 font-mono text-[11px] font-semibold"
|
||||
style={{background:"rgba(139,92,246,0.20)",color:VIOLET}}>
|
||||
{lastPush.sha}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium" style={{color:"rgba(255,255,255,0.90)"}}>
|
||||
{lastPush.message}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs" style={{color:"rgba(255,255,255,0.50)"}}>
|
||||
{lastPush.author} pushed to{" "}
|
||||
<span style={{color:"rgba(139,92,246,0.90)"}}>{lastPush.branch}</span>
|
||||
{" · "}{lastPush.repo}{" · "}{fmtRelative(lastPush.pushed_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Line stats — boxed ──────────────────────────────────── */}
|
||||
{hasLineStats ? (
|
||||
<div className="mx-auto flex max-w-xs items-center justify-center gap-8 rounded-xl px-8 py-4"
|
||||
|
|
|
|||
|
|
@ -115,10 +115,12 @@ function resetLabel(
|
|||
sessionWindow: SubscriptionWindowRead | null,
|
||||
modelsWindow: SubscriptionWindowRead | null,
|
||||
): string {
|
||||
const resetInMs = sessionWindow?.reset_in_ms ?? modelsWindow?.reset_in_ms;
|
||||
if (resetInMs == null) return "-";
|
||||
const date = fmtResetDate(resetInMs);
|
||||
return date ? `${fmtResetMs(resetInMs)} · Full reset at ${date}` : fmtResetMs(resetInMs);
|
||||
const countdownMs = sessionWindow?.reset_in_ms ?? modelsWindow?.reset_in_ms;
|
||||
if (countdownMs == null) return "-";
|
||||
const fullResetDate = fmtResetDate(modelsWindow?.reset_in_ms ?? countdownMs);
|
||||
return fullResetDate
|
||||
? `${fmtResetMs(countdownMs)} · Full: ${fullResetDate}`
|
||||
: fmtResetMs(countdownMs);
|
||||
}
|
||||
|
||||
function formatRemaining(pct: number | null): string {
|
||||
|
|
|
|||
Loading…
Reference in New Issue