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
30 changes: 25 additions & 5 deletions src/__tests__/date-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { describe, expect, it } from 'vitest';
import { apiDateString, dateStringForFormat } from '$lib/domain/shared';
import { maintenanceFormSchema } from '$lib/domain/maintenance';
import { complianceFormSchema } from '$lib/domain/compliance';
import { formatDate, type FormatConfig } from '$lib/helper/date.helper';
import { formatDate, parseDateForCalendar, type FormatConfig } from '$lib/helper/date.helper';

const formatConfig: FormatConfig = {
dateFormat: 'd. MMMM yyyy',
dateFormat: 'dd/MM/yyyy',
timezone: 'UTC',
locale: 'en',
currency: 'USD',
Expand All @@ -17,13 +17,33 @@ const formatConfig: FormatConfig = {
};

describe('localized date validation', () => {
it('restores calendar dates using the configured display format', () => {
const dayFirst = parseDateForCalendar('13/08/2026', formatConfig);
const monthFirst = parseDateForCalendar('08/13/2026', {
...formatConfig,
dateFormat: 'MM/dd/yyyy'
});

expect(dayFirst?.toString()).toBe('2026-08-13');
expect(monthFirst?.toString()).toBe('2026-08-13');
});

it('does not restore invalid calendar input values', () => {
expect(parseDateForCalendar('31/02/2026', formatConfig)).toBeUndefined();
expect(parseDateForCalendar(undefined, formatConfig)).toBeUndefined();
});

it.each([
['cs', '3. září 2026'],
['ru', '3. сентября 2026']
])('formats dates with %s month names', (locale, expected) => {
expect(formatDate(new Date('2026-09-03T12:00:00Z'), { ...formatConfig, locale })).toBe(
expected
);
expect(
formatDate(new Date('2026-09-03T12:00:00Z'), {
...formatConfig,
locale,
dateFormat: 'd. MMMM yyyy'
})
).toBe(expected);
});

it('accepts days greater than 12 in day-first formats', () => {
Expand Down
11 changes: 10 additions & 1 deletion src/lib/components/app/input.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import type { Component } from 'svelte';
import * as Popover from '$ui/popover';
import { Calendar } from '$lib/components/ui/calendar/index.js';
import { formatDateForCalendar } from '$lib/helper/format.helper';
import { formatDateForCalendar, parseDateForCalendar } from '$lib/helper/format.helper';
import * as m from '$lib/paraglide/messages';
import type { DateValue } from '@internationalized/date';

type InputType = Exclude<HTMLInputTypeAttribute, 'file'> | 'calendar';

Expand All @@ -27,6 +28,13 @@
}: Props = $props();

let open = $state(false);
let calendarValue = $state<DateValue>();

$effect(() => {
if (open) {
calendarValue = typeof value === 'string' ? parseDateForCalendar(value) : undefined;
}
});
</script>

<div id="input-wrapper" class="relative">
Expand Down Expand Up @@ -75,6 +83,7 @@
id="date-calendar"
type="single"
captionLayout="dropdown"
bind:value={calendarValue}
onValueChange={(v) => {
if (v) {
value = formatDateForCalendar(v);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<select {...props} {value} {onchange}>
{#each monthItems as monthItem (monthItem.value)}
<option
class="bg-popover text-popover-foreground"
value={monthItem.value}
selected={value !== undefined
? monthItem.value === value
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/ui/calendar/calendar-year-select.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<select {...props} {value}>
{#each yearItems as yearItem (yearItem.value)}
<option
class="bg-popover text-popover-foreground"
value={yearItem.value}
selected={value !== undefined
? yearItem.value === value
Expand Down
18 changes: 17 additions & 1 deletion src/lib/helper/date.helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DateValue } from '@internationalized/date';
import { CalendarDate, type DateValue } from '@internationalized/date';
import { format, parse } from 'date-fns';
import { formatInTimeZone, fromZonedTime } from 'date-fns-tz';
import { ar, cs, de, enUS, es, fi, fr, hi, hu, it, pt, ro, ru } from 'date-fns/locale';
Expand Down Expand Up @@ -66,6 +66,22 @@ export const formatDateForCalendar = (date: DateValue, config: FormatConfig): st
return format(dateObj, config.dateFormat, { locale: getDateFnsLocale(config.locale) });
};

export const parseDateForCalendar = (
date: string | undefined,
config: FormatConfig
): CalendarDate | undefined => {
if (!date) return undefined;

const parsedDate = parse(date, config.dateFormat, new Date());
if (Number.isNaN(parsedDate.getTime())) return undefined;

return new CalendarDate(
parsedDate.getFullYear(),
parsedDate.getMonth() + 1,
parsedDate.getDate()
);
};

export const parseDate = (date: string, config: FormatConfig) => {
const parsedDate = parse(date, config.dateFormat, new Date());
return fromZonedTime(parsedDate, config.timezone);
Expand Down
3 changes: 3 additions & 0 deletions src/lib/helper/format.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { FormatConfig } from './date.helper';
import {
formatDate as formatDatePure,
formatDateForCalendar as formatDateForCalendarPure,
parseDateForCalendar as parseDateForCalendarPure,
parseDate as parseDatePure
} from './date.helper';
export type { FormatConfig };
Expand All @@ -22,6 +23,8 @@ import {
export const formatDate = (date: Date | string) => formatDatePure(date, configs);
export const formatDateForCalendar = (date: import('@internationalized/date').DateValue) =>
formatDateForCalendarPure(date, configs);
export const parseDateForCalendar = (date: string | undefined) =>
parseDateForCalendarPure(date, configs);
export const parseDate = (date: string) => parseDatePure(date, configs);
export const getCurrencySymbol = (currency?: string) =>
getCurrencySymbolPure(currency || configs.currency);
Expand Down
Loading