"use client"; import { useMemo } from "react"; import { BarChart3, Minus, TrendingDown, TrendingUp } from "lucide-react"; import { CARD_TITLE_ROW, Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { ChartPeriodControls } from "@/components/dives/chart-period-controls"; import { ChartSkeleton } from "@/components/dives/chart-skeleton"; import { ChartStat, ChartStatRow } from "@/components/dives/chart-stat"; import { DiveActivityChart } from "@/components/dives/dive-activity-chart"; import { diveStatsAPI, DiveActivityPoint } from "@/lib/api/dive-stats"; import { type DiveActivitySummary, activityBars, activityDays, barCeiling, summarizeActivity, } from "@/lib/dive-activity"; import type { ChartScope } from "@/lib/chart-period"; import { DIVE_ACTIVITY_VIEW_KEY } from "@/lib/chart-period-view"; import { type ChartPeriodSource, useChartPeriodView, } from "@/hooks/useChartPeriodView"; import { cn } from "@/lib/utils"; const ACTIVITY_SOURCE: ChartPeriodSource = { load: () => diveStatsAPI.getDiveActivity(), failure: "Failed to fetch dive activity:", // A bar is a calendar bucket, so the anchor is the start of a day that has // diving in it. anchors: activityDays, storageKey: DIVE_ACTIVITY_VIEW_KEY, // Opens on the whole career, not on a year of it - the opposite default to the // gas card's, and deliberate. "How has my diving gone" is the question this // card exists for, and a bar per year answers it at any career length, where a // dot per dive over the same span is a smear. A single season is one click in. defaultScope: "all", }; // The Home page's how-much-am-I-diving card, and the counterpart to // `GasUseCard`: that one is about the quality of the diving, this one about the // quantity. They share their shape deliberately - the same header, the same stat // row, the same period controls in the same corner, the same All/Year/Month // scopes over the same shared `chart-period.ts` - because two charts on one page // that work differently cost more to read than either does alone. // // Where they differ is what a mark *is*. The gas chart plots dives, so its anchor // is one of their timestamps; here a bar is a calendar bucket, so the anchor is // the start of a day that has diving in it. Everything downstream of that - // stepping to the next period with dives, the dropdown of periods worth offering, // restoring the remembered one - is `useChartPeriodView`, shared by both cards. export function DiveActivityCard() { const view = useChartPeriodView(ACTIVITY_SOURCE); const { points, scope, anchor } = view; const bars = useMemo( () => activityBars(points ?? [], scope, anchor), [points, scope, anchor], ); const ceiling = useMemo( () => barCeiling(points ?? [], scope), [points, scope], ); const summary = useMemo( () => summarizeActivity(points ?? [], scope, anchor), [points, scope, anchor], ); return (
Dive Activity
{points === null ? ( ) : ( <> {summary && ( )} )}
); } // What one bar counts, for the stat that names the fullest of them. const BUSIEST_LABELS: Record = { all: "Busiest year", year: "Busiest month", month: "Busiest day", }; // The headline figures for what's on screen. The bars show the shape; these // answer "how much, and is it more than before", which a row of columns is // worst at answering precisely. function DiveActivitySummaryRow({ summary, scope, }: { summary: DiveActivitySummary; scope: ChartScope; }) { return ( }> {summary.dives} {summary.busiestLabel} {summary.busiestDives} {summary.busiestDives === 1 ? "dive" : "dives"} ); } // Change against the previous period with diving. // // Deliberately uncolored, the same call the gas card's `Change` makes and for // the same reason: the app has no good/bad status tokens, and teal is already // spoken for as "a dive" on the plot below. It also isn't obviously a good or // bad thing - a light year can be a house move rather than a slump - so the // arrow and the "vs 2025" carry the direction and nothing editorializes about it. function Change({ summary }: { summary: DiveActivitySummary }) { if (summary.change === null) return null; const Icon = summary.change === 0 ? Minus : summary.change < 0 ? TrendingDown : TrendingUp; return ( {summary.change === 0 ? `level with ${summary.previousLabel}` : `${summary.change > 0 ? "+" : "−"}${Math.abs(summary.change)} vs ${summary.previousLabel}`} ); }