From c12f94855d1744f90227a8d46fc466dcec6fbd6f Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 19 Apr 2025 21:44:40 -0400 Subject: [PATCH] feat: Refactor date handling in TransportationModal and add utility functions for date conversion and validation --- .../lib/components/TransportationModal.svelte | 116 +++++++++--------- frontend/src/lib/dateUtils.ts | 114 +++++++++++++++++ 2 files changed, 173 insertions(+), 57 deletions(-) create mode 100644 frontend/src/lib/dateUtils.ts diff --git a/frontend/src/lib/components/TransportationModal.svelte b/frontend/src/lib/components/TransportationModal.svelte index 3bd32e7..944b8e2 100644 --- a/frontend/src/lib/components/TransportationModal.svelte +++ b/frontend/src/lib/components/TransportationModal.svelte @@ -6,8 +6,14 @@ import { addToast } from '$lib/toasts'; let modal: HTMLDialogElement; import { t } from 'svelte-i18n'; - // @ts-ignore - import { DateTime } from 'luxon'; + import { + toLocalDatetime, + toUTCDatetime, + updateLocalDates, + updateUTCDates, + validateDateRange, + formatUTCDate + } from '$lib/dateUtils'; // Initialize with browser's timezone let selectedTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -30,21 +36,6 @@ let constrainDates: boolean = false; - // Convert a UTC ISO date to a datetime-local value in the specified timezone - function toLocalDatetime(utcDate: string | null, timezone: string = selectedTimezone): string { - if (!utcDate) return ''; - return DateTime.fromISO(utcDate, { zone: 'UTC' }) - .setZone(timezone) - .toISO({ suppressSeconds: true, includeOffset: false }) - .slice(0, 16); - } - - // Convert a local datetime to UTC - function toUTCDatetime(localDate: string, timezone: string = selectedTimezone): string | null { - if (!localDate) return null; - return DateTime.fromISO(localDate, { zone: timezone }).toUTC().toISO(); - } - // Initialize transportation object let transportation: Transportation = { id: transportationToEdit?.id || '', @@ -88,24 +79,13 @@ // Update local display dates whenever timezone or UTC dates change $: { - if (utcStartDate) { - localStartDate = toLocalDatetime(utcStartDate, selectedTimezone); - } - if (utcEndDate) { - localEndDate = toLocalDatetime(utcEndDate, selectedTimezone); - } - } - - // Explicitly watch for timezone changes to update displayed dates - $: { - // This will trigger whenever selectedTimezone changes - selectedTimezone; - if (utcStartDate) { - localStartDate = toLocalDatetime(utcStartDate); - } - if (utcEndDate) { - localEndDate = toLocalDatetime(utcEndDate); - } + const updatedDates = updateLocalDates({ + utcStartDate, + utcEndDate, + timezone: selectedTimezone + }); + localStartDate = updatedDates.localStartDate; + localEndDate = updatedDates.localEndDate; } onMount(async () => { @@ -117,13 +97,20 @@ // Initialize UTC dates from transportationToEdit if available if (transportationToEdit?.date) { utcStartDate = transportationToEdit.date; - localStartDate = toLocalDatetime(utcStartDate); } if (transportationToEdit?.end_date) { utcEndDate = transportationToEdit.end_date; - localEndDate = toLocalDatetime(utcEndDate); } + + // Update local dates based on UTC dates + const updatedDates = updateLocalDates({ + utcStartDate, + utcEndDate, + timezone: selectedTimezone + }); + localStartDate = updatedDates.localStartDate; + localEndDate = updatedDates.localEndDate; }); function close() { @@ -137,12 +124,18 @@ } // Update UTC dates when local dates change - function updateUTCDates() { - utcStartDate = localStartDate ? toUTCDatetime(localStartDate) : null; - utcEndDate = localEndDate ? toUTCDatetime(localEndDate) : null; + function handleLocalDateChange() { + const updated = updateUTCDates({ + localStartDate, + localEndDate, + timezone: selectedTimezone + }); + utcStartDate = updated.utcStartDate; + utcEndDate = updated.utcEndDate; } async function geocode(e: Event | null) { + // Geocoding logic unchanged if (e) { e.preventDefault(); } @@ -226,7 +219,7 @@ Math.round(transportation.destination_longitude * 1e6) / 1e6; } - // Validate dates + // Validate dates using utility function if (localEndDate && !localStartDate) { addToast('error', $t('adventures.start_date_required')); return; @@ -238,11 +231,9 @@ utcEndDate = utcStartDate; } - if ( - localStartDate && - localEndDate && - DateTime.fromISO(localStartDate).toMillis() > DateTime.fromISO(localEndDate).toMillis() - ) { + // Validate date range + const validation = validateDateRange(localStartDate, localEndDate); + if (!validation.valid) { addToast('error', $t('adventures.start_before_end_error')); return; } @@ -272,9 +263,15 @@ // Update the UTC dates with the values from the server utcStartDate = data.date; utcEndDate = data.end_date; - // Update displayed dates - localStartDate = toLocalDatetime(utcStartDate); - localEndDate = toLocalDatetime(utcEndDate); + + // Update displayed dates using utility function + const updatedDates = updateLocalDates({ + utcStartDate, + utcEndDate, + timezone: selectedTimezone + }); + localStartDate = updatedDates.localStartDate; + localEndDate = updatedDates.localEndDate; addToast('success', $t('adventures.adventure_created')); dispatch('save', transportation); @@ -296,9 +293,15 @@ // Update the UTC dates with the values from the server utcStartDate = data.date; utcEndDate = data.end_date; - // Update displayed dates - localStartDate = toLocalDatetime(utcStartDate); - localEndDate = toLocalDatetime(utcEndDate); + + // Update displayed dates using utility function + const updatedDates = updateLocalDates({ + utcStartDate, + utcEndDate, + timezone: selectedTimezone + }); + localStartDate = updatedDates.localStartDate; + localEndDate = updatedDates.localEndDate; addToast('success', $t('adventures.adventure_updated')); dispatch('save', transportation); @@ -481,7 +484,7 @@ id="date" name="date" bind:value={localStartDate} - on:change={updateUTCDates} + on:change={handleLocalDateChange} min={constrainDates ? fullStartDate : ''} max={constrainDates ? fullEndDate : ''} class="input input-bordered w-full max-w-xs mt-1" @@ -502,7 +505,7 @@ min={constrainDates ? localStartDate : ''} max={constrainDates ? fullEndDate : ''} bind:value={localEndDate} - on:change={updateUTCDates} + on:change={handleLocalDateChange} class="input input-bordered w-full max-w-xs mt-1" /> @@ -529,15 +532,14 @@ {#if utcStartDate}
- UTC Time: {DateTime.fromISO(utcStartDate).toISO().slice(0, 16).replace('T', ' ')} + UTC Time: {formatUTCDate(utcStartDate)} {#if utcEndDate && utcEndDate !== utcStartDate} - to {DateTime.fromISO(utcEndDate).toISO().slice(0, 16).replace('T', ' ')} + to {formatUTCDate(utcEndDate)} {/if}
{/if} -
diff --git a/frontend/src/lib/dateUtils.ts b/frontend/src/lib/dateUtils.ts new file mode 100644 index 0000000..fdf2c8c --- /dev/null +++ b/frontend/src/lib/dateUtils.ts @@ -0,0 +1,114 @@ +// @ts-ignore +import { DateTime } from 'luxon'; + +/** + * Convert a UTC ISO date to a datetime-local value in the specified timezone + * @param utcDate - UTC date in ISO format or null + * @param timezone - Target timezone (defaults to browser timezone) + * @returns Formatted local datetime string for input fields (YYYY-MM-DDTHH:MM) + */ +export function toLocalDatetime( + utcDate: string | null, + timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone +): string { + if (!utcDate) return ''; + return DateTime.fromISO(utcDate, { zone: 'UTC' }) + .setZone(timezone) + .toISO({ suppressSeconds: true, includeOffset: false }) + .slice(0, 16); +} + +/** + * Convert a local datetime to UTC + * @param localDate - Local datetime string in ISO format + * @param timezone - Source timezone (defaults to browser timezone) + * @returns UTC datetime in ISO format or null + */ +export function toUTCDatetime( + localDate: string, + timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone +): string | null { + if (!localDate) return null; + return DateTime.fromISO(localDate, { zone: timezone }).toUTC().toISO(); +} + +/** + * Updates local datetime display values based on UTC values and timezone + * @param params Object containing UTC dates and timezone + * @returns Object with updated local datetime strings + */ +export function updateLocalDates({ + utcStartDate, + utcEndDate, + timezone +}: { + utcStartDate: string | null; + utcEndDate: string | null; + timezone: string; +}) { + return { + localStartDate: toLocalDatetime(utcStartDate, timezone), + localEndDate: toLocalDatetime(utcEndDate, timezone) + }; +} + +/** + * Updates UTC datetime values based on local values and timezone + * @param params Object containing local dates and timezone + * @returns Object with updated UTC datetime strings + */ +export function updateUTCDates({ + localStartDate, + localEndDate, + timezone +}: { + localStartDate: string; + localEndDate: string; + timezone: string; +}) { + return { + utcStartDate: toUTCDatetime(localStartDate, timezone), + utcEndDate: toUTCDatetime(localEndDate, timezone) + }; +} + +/** + * Validate date ranges + * @param startDate - Start date string + * @param endDate - End date string + * @returns Object with validation result and error message + */ +export function validateDateRange( + startDate: string, + endDate: string +): { valid: boolean; error?: string } { + if (endDate && !startDate) { + return { + valid: false, + error: 'Start date is required when end date is provided' + }; + } + + if ( + startDate && + endDate && + DateTime.fromISO(startDate).toMillis() > DateTime.fromISO(endDate).toMillis() + ) { + return { + valid: false, + error: 'Start date must be before end date' + }; + } + + return { valid: true }; +} + +/** + * Format UTC date for display + * @param utcDate - UTC date in ISO format + * @returns Formatted date string without seconds (YYYY-MM-DD HH:MM) + */ +export function formatUTCDate(utcDate: string | null): string { + if (!utcDate) return ''; + return DateTime.fromISO(utcDate).toISO().slice(0, 16).replace('T', ' '); +}