mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
Merge v1.25.3: honor the 24h time-format preference app-wide
This commit is contained in:
commit
d0eced41f9
9 files changed
+131
-26
No files matched your search
@@ -11,7 +11,8 @@ import {
|
||||
} from "recharts";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
|
||||
import { hourCycleOptions } from "@/lib/format-locale";
|
||||
import { STAGE_COLORS } from "./sleep-stage-stacked-bar";
|
||||
|
||||
/**
|
||||
@@ -112,6 +113,7 @@ function formatMinutes(total: number, locale: string): string {
|
||||
|
||||
export function SleepHypnogram({ session }: SleepHypnogramProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const timeFormat = useTimeFormatPreference();
|
||||
|
||||
const stageLabels: Record<string, string> = {
|
||||
DEEP: t("insights.sleep.stages.deep"),
|
||||
@@ -192,8 +194,9 @@ export function SleepHypnogram({ session }: SleepHypnogramProps) {
|
||||
new Intl.DateTimeFormat(locale === "de" ? "de-DE" : "en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
...hourCycleOptions(timeFormat),
|
||||
}),
|
||||
[locale],
|
||||
[locale, timeFormat],
|
||||
);
|
||||
|
||||
// Per-stage breakdown with percentage of the night's total tracked time.
|
||||
|
||||
@@ -17,7 +17,11 @@ import {
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
|
||||
import {
|
||||
hourCycleOptions,
|
||||
type TimeFormatPreference,
|
||||
} from "@/lib/format-locale";
|
||||
import type { WorkoutDetailPayload } from "@/hooks/use-workouts";
|
||||
|
||||
/**
|
||||
@@ -103,6 +107,7 @@ function formatDateRange(
|
||||
startedAt: string,
|
||||
endedAt: string,
|
||||
locale: string,
|
||||
timeFormat: TimeFormatPreference,
|
||||
): string {
|
||||
const start = new Date(startedAt);
|
||||
const end = new Date(endedAt);
|
||||
@@ -115,6 +120,7 @@ function formatDateRange(
|
||||
const timeFmt = new Intl.DateTimeFormat(locale, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
...hourCycleOptions(timeFormat),
|
||||
});
|
||||
return `${dateFmt.format(start)} · ${timeFmt.format(start)} – ${timeFmt.format(end)}`;
|
||||
}
|
||||
@@ -139,6 +145,7 @@ function renderSportIconBadge(sportType: string) {
|
||||
|
||||
export function WorkoutDetailHeader({ workout }: WorkoutDetailHeaderProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const timeFormat = useTimeFormatPreference();
|
||||
const sportLabelKey = `insights.workouts.sport.${workout.sportType}`;
|
||||
const sportLabel = t(sportLabelKey);
|
||||
const sportName =
|
||||
@@ -158,7 +165,12 @@ export function WorkoutDetailHeader({ workout }: WorkoutDetailHeaderProps) {
|
||||
{sportName}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{formatDateRange(workout.startedAt, workout.endedAt, locale)}
|
||||
{formatDateRange(
|
||||
workout.startedAt,
|
||||
workout.endedAt,
|
||||
locale,
|
||||
timeFormat,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end text-right">
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
|
||||
import {
|
||||
hourCycleOptions,
|
||||
type TimeFormatPreference,
|
||||
} from "@/lib/format-locale";
|
||||
import type { WorkoutListEntry } from "@/hooks/use-workouts";
|
||||
|
||||
/**
|
||||
@@ -76,7 +80,11 @@ function formatEnergy(kcal: number, locale: string): string {
|
||||
}).format(Math.round(kcal));
|
||||
}
|
||||
|
||||
function formatDate(iso: string, locale: string): string {
|
||||
function formatDate(
|
||||
iso: string,
|
||||
locale: string,
|
||||
timeFormat: TimeFormatPreference,
|
||||
): string {
|
||||
const d = new Date(iso);
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
weekday: "short",
|
||||
@@ -84,6 +92,7 @@ function formatDate(iso: string, locale: string): string {
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
...hourCycleOptions(timeFormat),
|
||||
}).format(d);
|
||||
}
|
||||
|
||||
@@ -94,6 +103,7 @@ export interface WorkoutListProps {
|
||||
|
||||
export function WorkoutList({ workouts, className }: WorkoutListProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const timeFormat = useTimeFormatPreference();
|
||||
|
||||
return (
|
||||
<ul
|
||||
@@ -129,7 +139,7 @@ export function WorkoutList({ workouts, className }: WorkoutListProps) {
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span className="truncate font-medium">{sportName}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{formatDate(workout.startedAt, locale)}
|
||||
{formatDate(workout.startedAt, locale, timeFormat)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-0.5 text-xs">
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { ChevronDown, Loader2, ShieldCheck } from "lucide-react";
|
||||
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -54,6 +54,7 @@ export function SecurityActivityCard({
|
||||
isAuthenticated: boolean;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
// Collapsed by default — the feed opens only on demand, keeping the security
|
||||
// surface skimmable. UI-only state; nothing is persisted across reloads.
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -138,7 +139,7 @@ export function SecurityActivityCard({
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground shrink-0 text-xs">
|
||||
{new Date(e.createdAt).toLocaleString()}
|
||||
{fmt.dateTime(e.createdAt)}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ChevronDown, Loader2, MonitorSmartphone } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet, apiDelete } from "@/lib/api/api-fetch";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -36,6 +36,7 @@ export function SecuritySessionsCard({
|
||||
isAuthenticated: boolean;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
const queryClient = useQueryClient();
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
// Collapsed by default — the list opens only when the user asks for it, so
|
||||
@@ -158,7 +159,7 @@ export function SecuritySessionsCard({
|
||||
{s.lastActiveAt && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.security.lastActive", {
|
||||
time: new Date(s.lastActiveAt).toLocaleString(),
|
||||
time: fmt.dateTime(s.lastActiveAt),
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useTimeFormatPreference,
|
||||
} from "@/lib/i18n/context";
|
||||
import { formatDate, resolveDateLocale } from "@/lib/date-format";
|
||||
import { hourCycleOptions } from "@/lib/format-locale";
|
||||
|
||||
/**
|
||||
* Dependency-free datetime input that DISPLAYS the value in the user's
|
||||
@@ -221,20 +222,6 @@ function formatLocalDateTime(
|
||||
return `${datePart} ${timePart}`;
|
||||
}
|
||||
|
||||
/** Mirror of `format-locale.ts` `hourCycleOptions`, kept local to the field. */
|
||||
function hourCycleOptions(
|
||||
timeFormat: ReturnType<typeof useTimeFormatPreference>,
|
||||
): Intl.DateTimeFormatOptions {
|
||||
switch (timeFormat) {
|
||||
case "H12":
|
||||
return { hour12: true };
|
||||
case "H24":
|
||||
return { hourCycle: "h23" };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Split a local `yyyy-MM-ddTHH:mm` string into its calendar parts. */
|
||||
function parseLocalDateTime(value: string): {
|
||||
date: string;
|
||||
|
||||
Reference in new issue
Block a user