fix(workouts): render workout dates in the profile timezone

The workout list, the workout header and the dashboard tile formatted
instants through the shared formatter cache without a timeZone, so they
followed the browser's zone. The year comparison in the list used the
browser's calendar as well. All three now take the profile zone, and the
timezone guard also matches getDateTimeFormat(locale, options) calls that
leave the zone out.
This commit is contained in:
Marc-André Bombeck committed 2026-10-19 14:56:53 +02:00
1 parent 01bf8fbb64
commit ff3559c169
6 files changed
+72 -12

No files matched your search

@@ -4,7 +4,7 @@ import Link from "next/link";
import { Activity } from "lucide-react";
import { formatDurationMinutes } from "@/lib/i18n/duration";
import { useTranslations } from "@/lib/i18n/context";
import { useDisplayTimezone, useTranslations } from "@/lib/i18n/context";
import { useWorkouts, type WorkoutListEntry } from "@/hooks/use-workouts";
import { cn } from "@/lib/utils";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
@@ -32,9 +32,10 @@ import { iconForSport } from "@/lib/workouts/sport-icons";
* round-trip per session.
*/
function formatDayLabel(iso: string, locale: string): string {
function formatDayLabel(iso: string, locale: string, timeZone: string): string {
const d = new Date(iso);
return getDateTimeFormat(locale, {
timeZone,
weekday: "short",
day: "2-digit",
month: "short",
@@ -61,6 +62,7 @@ function renderRow(workout: WorkoutListEntry, sportName: string) {
export function RecentWorkoutsTile() {
const { t, locale } = useTranslations();
const timeZone = useDisplayTimezone();
const { data, isError, refetch } = useWorkouts({ limit: 3 });
// A query's `isLoading` is not a hydration-safe branch: TanStack reports the
@@ -145,7 +147,7 @@ export function RecentWorkoutsTile() {
>
{renderRow(workout, sportName)}
<span className="text-muted-foreground shrink-0 text-xs tabular-nums">
{formatDayLabel(workout.startedAt, locale)}
{formatDayLabel(workout.startedAt, locale, timeZone)}
</span>
<span className="ml-1 shrink-0 text-xs font-medium tabular-nums">
{formatDurationMinutes(
@@ -126,16 +126,19 @@ export function formatDateRange(
endedAt: string,
locale: string,
timeFormat: TimeFormatPreference,
timeZone: string,
): string {
const start = new Date(startedAt);
const end = new Date(endedAt);
const dateFmt = getDateTimeFormat(locale, {
timeZone,
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
});
const timeFmt = getDateTimeFormat(locale, {
timeZone,
hour: "2-digit",
minute: "2-digit",
...hourCycleOptions(timeFormat),
@@ -1,6 +1,10 @@
"use client";
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
import {
useDisplayTimezone,
useTimeFormatPreference,
useTranslations,
} from "@/lib/i18n/context";
import { MEASUREMENT_SOURCE_LIST_LABEL_KEYS } from "@/lib/i18n/source-labels";
import { iconForSport } from "@/lib/workouts/sport-icons";
import type { WorkoutDetailPayload } from "@/hooks/use-workouts";
@@ -32,6 +36,7 @@ function renderSportIconBadge(sportType: string) {
export function WorkoutDetailHeader({ workout }: WorkoutDetailHeaderProps) {
const { t, locale } = useTranslations();
const timeFormat = useTimeFormatPreference();
const timeZone = useDisplayTimezone();
const { preference } = useUnitDisplay();
const sportLabelKey = `insights.workouts.sport.${workout.sportType}`;
const sportLabel = t(sportLabelKey);
@@ -68,6 +73,7 @@ export function WorkoutDetailHeader({ workout }: WorkoutDetailHeaderProps) {
workout.endedAt,
locale,
timeFormat,
timeZone,
)}
</p>
</div>
+17 -3
View File
@@ -7,7 +7,11 @@ import { HeartPulse, Loader2, Map as MapIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { getNumberFormat, getDateTimeFormat } from "@/lib/intl/formatter-cache";
import { formatDurationMinutes } from "@/lib/i18n/duration";
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
import {
useDisplayTimezone,
useTimeFormatPreference,
useTranslations,
} from "@/lib/i18n/context";
import {
hourCycleOptions,
type TimeFormatPreference,
@@ -40,12 +44,16 @@ function formatDate(
iso: string,
locale: string,
timeFormat: TimeFormatPreference,
timeZone: string,
): string {
const d = new Date(iso);
// Show the year whenever the workout is not from the current year, so a
// "Do., 16. Dez." from a previous year is never mistaken for this year's.
const showYear = d.getFullYear() !== new Date().getFullYear();
const yearOf = (value: Date) =>
getDateTimeFormat(locale, { timeZone, year: "numeric" }).format(value);
const showYear = yearOf(d) !== yearOf(new Date());
return getDateTimeFormat(locale, {
timeZone,
weekday: "short",
day: "2-digit",
month: "short",
@@ -75,6 +83,7 @@ export function WorkoutList({
}: WorkoutListProps) {
const { t, locale } = useTranslations();
const timeFormat = useTimeFormatPreference();
const timeZone = useDisplayTimezone();
const { preference } = useUnitDisplay();
return (
@@ -141,7 +150,12 @@ export function WorkoutList({
stays readable beside the stats column. */}
<span className="text-muted-foreground text-xs">
<DayLinkAt at={workout.startedAt} className="z-10">
{formatDate(workout.startedAt, locale, timeFormat)}
{formatDate(
workout.startedAt,
locale,
timeFormat,
timeZone,
)}
</DayLinkAt>
</span>
</div>