Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/components/Routines/screens/Detail/RoutineDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import i18n from "@/i18n";
import React from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { dateToLocale } from "@/core/lib/date";
import { formatNaiveDate } from "@/core/lib/date";
import { makeLink, WgerLink } from "@/core/lib/url";

export const RoutineDetail = () => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const params = useParams<{ routineId: string }>();
const routineId = parseInt(params.routineId ?? '');
if (Number.isNaN(routineId)) {
Expand All @@ -25,7 +25,7 @@ export const RoutineDetail = () => {
const routineQuery = useRoutineDetailQuery(routineId);

const routine = routineQuery.data;
const subtitle = routine !== undefined ? `${dateToLocale(routine!.start)} - ${dateToLocale(routine!.end)} (${routine?.durationText})` : '';
const subtitle = routine !== undefined ? `${formatNaiveDate(routine!.start, i18n.language)} - ${formatNaiveDate(routine!.end, i18n.language)} (${routine?.durationText})` : '';
const chip = routine?.isTemplate
? <Chip color="info" size="small" label={t('routines.template')} />
: null;
Expand Down
4 changes: 2 additions & 2 deletions src/components/Routines/screens/Detail/TemplateDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DayDetailsCard } from "@/components/Routines/widgets/RoutineDetailsCard
import React from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { dateToLocale } from "@/core/lib/date";
import { formatNaiveDate } from "@/core/lib/date";
import { makeLink, WgerLink } from "@/core/lib/url";

export const TemplateDetail = () => {
Expand All @@ -34,7 +34,7 @@ export const TemplateDetail = () => {
mainContent={
<Stack spacing={2}>
<Typography variant={"subtitle1"}>
{dateToLocale(routine!.start)} - {dateToLocale(routine!.end)} ({routine!.durationText})
{formatNaiveDate(routine!.start, i18n.language)} - {formatNaiveDate(routine!.end, i18n.language)} ({routine!.durationText})
</Typography>

{routine!.description !== ''
Expand Down
4 changes: 2 additions & 2 deletions src/components/Routines/screens/Overview/RoutineOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AddRoutineFab } from "@/components/Routines/screens/Overview/Fab";
import { useRoutinesShallowQuery } from "@/components/Routines/queries";
import React from "react";
import { useTranslation } from "react-i18next";
import { dateToLocale } from "@/core/lib/date";
import { formatNaiveDate } from "@/core/lib/date";
import { makeLink, WgerLink } from "@/core/lib/url";

export const RoutineList = (props: {
Expand Down Expand Up @@ -42,7 +42,7 @@ export const RoutineList = (props: {
<ListItemButton component="a" href={detailUrl}>
<ListItemText
primary={<>{primaryText} {chipTemplate} {chipVisibility}</>}
secondary={`${props.routine.durationText} (${dateToLocale(props.routine.start)} - ${dateToLocale(props.routine.end)})`}
secondary={`${props.routine.durationText} (${formatNaiveDate(props.routine.start, i18n.language)} - ${formatNaiveDate(props.routine.end, i18n.language)})`}
/>
<ChevronRightIcon />
</ListItemButton>
Expand Down
30 changes: 29 additions & 1 deletion src/core/lib/date.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { calculatePastDate, dateTimeToHHMM, dateToYYYYMMDD } from "@/core/lib/date";
import { calculatePastDate, dateTimeToHHMM, dateToYYYYMMDD, formatNaiveDate } from "@/core/lib/date";

describe("test date utility", () => {

Expand Down Expand Up @@ -53,4 +53,32 @@ describe('calculatePastDate', () => {
const result = calculatePastDate('lastYear', new Date('2023-02-14'));
expect(result).toStrictEqual('2022-02-14');
});
});

describe('formatNaiveDate - Timezone-Agnostic Validation', () => {
it('should format standard JS Date objects timezone-agnostically', () => {
// E.g. date parsed in UTC representing "2026-05-12"
const dateObject = new Date('2026-05-12T00:00:00Z');

const formattedUS = formatNaiveDate(dateObject, 'en-US');
expect(formattedUS).toBe('05/12/2026');

const formattedIT = formatNaiveDate(dateObject, 'it-IT');
expect(formattedIT).toBe('12/05/2026');
});

it('should correctly format month boundaries without falling back to previous/next month', () => {
// First day of month
expect(formatNaiveDate(new Date('2026-01-01T00:00:00Z'), 'en-US')).toBe('01/01/2026');
// Last day of month
expect(formatNaiveDate(new Date('2026-12-31T00:00:00Z'), 'en-US')).toBe('12/31/2026');
// Leap year date
expect(formatNaiveDate(new Date('2024-02-29T00:00:00Z'), 'en-US')).toBe('02/29/2024');
});

it('should return an empty string for invalid dates, null, or undefined', () => {
expect(formatNaiveDate(null)).toBe('');
expect(formatNaiveDate(undefined)).toBe('');
expect(formatNaiveDate(new Date('invalid-date'))).toBe('');
});
});
24 changes: 24 additions & 0 deletions src/core/lib/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,30 @@ export function dateToLocale(dateTime: Date | null, locale?: string, options?: I
return dateTime.toLocaleString(locale ? [locale] : [], options);
}

/**
* Formats a standard JS Date object into a localized date format without applying
* local browser timezone translation (i.e. timezone-agnostic).
*
* @param date - The JS Date object representing a naive date
* @param locale - Optional locale for formatting (defaults to active i18n language or 'en-US')
* @returns The formatted date string, or an empty string if the input is invalid
*/
export function formatNaiveDate(date: Date | null | undefined, locale?: string): string {
if (!date || isNaN(date.getTime())) {
return '';
}

const resolvedLocale = locale ?? i18n.language ?? 'en-US';

// Format explicitly using the UTC timezone and consistent fields
return date.toLocaleDateString(resolvedLocale, {
timeZone: 'UTC',
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}

/*
* Converts a date object to a non localized string in the format HH:MM
*/
Expand Down