mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Say when a session has heart-rate figures but no profile behind them
The heart-rate card hid itself whenever the server resolved no curve. That is right for a session with no heart rate at all, but wrong for the common case it also covered: a ride logged from a chest strap through a provider that reports an average and a peak and nothing in between. The stats grid shows those two numbers, the card next to them renders nothing, and the page reads as if it has no heart-rate section rather than as if the shape behind the numbers is not on record. Three states now. A curve when there is one; a named absence when the session carries heart-rate figures without a profile; nothing at all when nothing was ever measured, since there is no absence to report there and an empty card would be a shell.
This commit is contained in:
1 parent
0c99c27bfc
commit
cea330bf70
8 files changed
+66
-14
No files matched your search
@@ -207,8 +207,27 @@ describe("<WorkoutDetailRoute>", () => {
|
||||
});
|
||||
|
||||
describe("<WorkoutDetailHrSection>", () => {
|
||||
it("returns null without an HR series", () => {
|
||||
expect(render(<WorkoutDetailHrSection workout={FIXTURE} />)).toBe("");
|
||||
it("names the absence when the session has heart-rate figures but no profile", () => {
|
||||
// The stats grid is showing an average and a peak for this session,
|
||||
// so the reader can see heart rate WAS recorded. Rendering nothing
|
||||
// here would read as "this page has no heart-rate section" rather
|
||||
// than as "the shape behind those numbers is not on record".
|
||||
const html = render(<WorkoutDetailHrSection workout={FIXTURE} />);
|
||||
expect(html).toContain('data-slot="workout-detail-hr-empty"');
|
||||
expect(html).toContain("No reading-by-reading heart-rate data");
|
||||
expect(html).not.toContain('data-slot="workout-hr-chart"');
|
||||
});
|
||||
|
||||
it("returns null when the session carries no heart rate at all", () => {
|
||||
// Nothing was measured, so there is no absence to report and an
|
||||
// empty card would be a shell.
|
||||
const noHr: WorkoutDetailPayload = {
|
||||
...FIXTURE,
|
||||
avgHr: null,
|
||||
maxHr: null,
|
||||
minHr: null,
|
||||
};
|
||||
expect(render(<WorkoutDetailHrSection workout={noHr} />)).toBe("");
|
||||
});
|
||||
|
||||
it("renders the card and a provenance chip for a pulse-window series", () => {
|
||||
|
||||
@@ -37,15 +37,33 @@ export interface WorkoutDetailHrSectionProps {
|
||||
* Heart-rate curve card — mean line + optional min→max envelope + %HRmax
|
||||
* zone bands + average and peak reference lines. A muted provenance chip
|
||||
* discloses when the curve was reconstructed from pulse data around the
|
||||
* session rather than the workout's own sensor stream. Returns `null`
|
||||
* when no series is available (hide, don't render empty).
|
||||
* session rather than the workout's own sensor stream.
|
||||
*
|
||||
* Three states, and the middle one is the reason this is not a plain
|
||||
* hide-when-empty section:
|
||||
*
|
||||
* - a curve, when the server resolved one;
|
||||
* - a named absence, when the session HAS heart-rate figures (the
|
||||
* stats grid is showing an average, a peak) but no reading-by-reading
|
||||
* profile behind them. Silence there reads as "this page has no
|
||||
* heart-rate section", which is wrong — the data exists, the shape of
|
||||
* it does not, and saying so is the difference between an honest gap
|
||||
* and an invisible one;
|
||||
* - nothing at all, when the session carries no heart rate of any kind.
|
||||
* There is no absence to report where nothing was ever measured, and
|
||||
* an empty card would be a shell (page contract: hide, don't render
|
||||
* empty).
|
||||
*/
|
||||
export function WorkoutDetailHrSection({
|
||||
workout,
|
||||
}: WorkoutDetailHrSectionProps) {
|
||||
const { t } = useTranslations();
|
||||
const series = workout.hrSeries;
|
||||
if (!series || series.points.length < 2) return null;
|
||||
const hasCurve = Boolean(series && series.points.length >= 2);
|
||||
const hasHeartRateAtAll =
|
||||
workout.avgHr != null || workout.maxHr != null || workout.minHr != null;
|
||||
|
||||
if (!hasCurve && !hasHeartRateAtAll) return null;
|
||||
|
||||
return (
|
||||
<Card data-slot="workout-detail-hr">
|
||||
@@ -55,7 +73,7 @@ export function WorkoutDetailHrSection({
|
||||
title={t("insights.workouts.detail.hrChartTitle")}
|
||||
titleAs="h2"
|
||||
/>
|
||||
{series.source === "pulse_window" ? (
|
||||
{hasCurve && series!.source === "pulse_window" ? (
|
||||
<p
|
||||
data-slot="workout-detail-hr-provenance"
|
||||
className="text-muted-foreground text-xs"
|
||||
@@ -65,14 +83,23 @@ export function WorkoutDetailHrSection({
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<WorkoutHrChart
|
||||
points={series.points}
|
||||
bucketSec={series.bucketSec}
|
||||
envelope={series.envelope}
|
||||
avgHr={workout.avgHr}
|
||||
maxHr={workout.maxHr}
|
||||
zones={workout.zones?.zones ?? null}
|
||||
/>
|
||||
{hasCurve ? (
|
||||
<WorkoutHrChart
|
||||
points={series!.points}
|
||||
bucketSec={series!.bucketSec}
|
||||
envelope={series!.envelope}
|
||||
avgHr={workout.avgHr}
|
||||
maxHr={workout.maxHr}
|
||||
zones={workout.zones?.zones ?? null}
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
data-slot="workout-detail-hr-empty"
|
||||
className="text-muted-foreground text-sm"
|
||||
>
|
||||
{t("insights.workouts.detail.hrNoProfile")}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user