activity-stats: shared StatRow + canonical metrics across surfaces
Implements the activity-stats change (specs/activity-stats): - gpx: `movingTime(tracks)` — moving seconds from trackpoint timestamps, excluding stationary spans + long gaps; null when no timestamps. - stats.ts: pure formatters (distance/elevation/duration/speed/pace), sport-aware `deriveRate` (pace for foot sports, speed otherwise, speed default), and `activityStatItems` encoding the canonical order (distance · time · [moving] · pace/speed · ascent · descent; compact subset). - StatRow: one shared presentational component (size sm/lg), adopted on the activity detail (full set; loader derives moving time), route detail (distance/ascent/descent), feed card + profile list (compact). - i18n: journal.stats.* in en + de. Tests: movingTime (stationary/gap exclusion, moving ≤ elapsed), deriveRate, formatters, activityStatItems ordering + compact, StatRow render (jsdom). typecheck + lint + unit (gpx 63, journal 311) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ede263d4d1
commit
0325d01bca
15 changed files with 482 additions and 67 deletions
34
apps/journal/app/components/StatRow.tsx
Normal file
34
apps/journal/app/components/StatRow.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { StatItem } from "~/lib/stats";
|
||||
|
||||
/**
|
||||
* The one shared headline-stat presentation. Surfaces pass an ordered list of
|
||||
* {value, label} items (built via `activityStatItems`); the component never
|
||||
* fetches or formats. `size="lg"` is the detail-page treatment; the default
|
||||
* compact size is for feed cards and the profile list.
|
||||
*/
|
||||
export function StatRow({
|
||||
items,
|
||||
size = "sm",
|
||||
className,
|
||||
}: {
|
||||
items: StatItem[];
|
||||
size?: "sm" | "lg";
|
||||
className?: string;
|
||||
}) {
|
||||
if (items.length === 0) return null;
|
||||
const valueCls =
|
||||
size === "lg"
|
||||
? "text-2xl font-bold text-gray-900"
|
||||
: "text-sm font-semibold text-gray-900";
|
||||
const gap = size === "lg" ? "gap-6" : "gap-x-4 gap-y-1";
|
||||
return (
|
||||
<dl className={`flex flex-wrap ${gap}${className ? ` ${className}` : ""}`}>
|
||||
{items.map((it) => (
|
||||
<div key={it.label}>
|
||||
<dd className={valueCls}>{it.value}</dd>
|
||||
<dt className="text-xs text-gray-500">{it.label}</dt>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue