diff --git a/frontend/package.json b/frontend/package.json index 918a869..a486beb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -43,6 +43,7 @@ "dompurify": "^3.2.4", "emoji-picker-element": "^1.26.0", "gsap": "^3.12.7", + "luxon": "^3.6.1", "marked": "^15.0.4", "psl": "^1.15.0", "qrcode": "^1.5.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index f41d2f5..5480141 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: gsap: specifier: ^3.12.7 version: 3.12.7 + luxon: + specifier: ^3.6.1 + version: 3.6.1 marked: specifier: ^15.0.4 version: 15.0.4 @@ -1469,6 +1472,10 @@ packages: lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} + luxon@3.6.1: + resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} + engines: {node: '>=12'} + magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} @@ -3514,6 +3521,8 @@ snapshots: dependencies: es5-ext: 0.10.64 + luxon@3.6.1: {} + magic-string@0.30.10: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 diff --git a/frontend/src/lib/components/TimezoneSelector.svelte b/frontend/src/lib/components/TimezoneSelector.svelte new file mode 100644 index 0000000..c0738b8 --- /dev/null +++ b/frontend/src/lib/components/TimezoneSelector.svelte @@ -0,0 +1,92 @@ + + +
+ + + +
(dropdownOpen = !dropdownOpen)} + > + {selectedTimezone} + + + +
+ + + {#if dropdownOpen} +
+ +
+ +
+ + + {#if filteredTimezones.length > 0} + + {:else} +
No timezones found
+ {/if} +
+ {/if} +
diff --git a/frontend/src/lib/components/TransportationModal.svelte b/frontend/src/lib/components/TransportationModal.svelte index 1d4a516..3bd32e7 100644 --- a/frontend/src/lib/components/TransportationModal.svelte +++ b/frontend/src/lib/components/TransportationModal.svelte @@ -6,36 +6,53 @@ import { addToast } from '$lib/toasts'; let modal: HTMLDialogElement; import { t } from 'svelte-i18n'; + // @ts-ignore + import { DateTime } from 'luxon'; + + // Initialize with browser's timezone + let selectedTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // Store the UTC dates as source of truth + let utcStartDate: string | null = null; + let utcEndDate: string | null = null; + + // Local display values + let localStartDate: string = ''; + let localEndDate: string = ''; import MarkdownEditor from './MarkdownEditor.svelte'; import { appVersion } from '$lib/config'; import { DefaultMarker, MapLibre } from 'svelte-maplibre'; + import TimezoneSelector from './TimezoneSelector.svelte'; export let collection: Collection; export let transportationToEdit: Transportation | null = null; let constrainDates: boolean = false; - // Format date as local datetime - // Convert an ISO date to a datetime-local value in local time. - function toLocalDatetime(value: string | null): string { - if (!value) return ''; - const date = new Date(value); - // Adjust the time by subtracting the timezone offset. - date.setMinutes(date.getMinutes() - date.getTimezoneOffset()); - // Return format YYYY-MM-DDTHH:mm - return date.toISOString().slice(0, 16); + // 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 || '', type: transportationToEdit?.type || '', name: transportationToEdit?.name || '', description: transportationToEdit?.description || '', - date: transportationToEdit?.date ? toLocalDatetime(transportationToEdit.date) : null, - end_date: transportationToEdit?.end_date - ? toLocalDatetime(transportationToEdit.end_date) - : null, + date: null, + end_date: null, rating: transportationToEdit?.rating || 0, link: transportationToEdit?.link || '', flight_number: transportationToEdit?.flight_number || '', @@ -69,13 +86,44 @@ } } - console.log(transportation); + // 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); + } + } onMount(async () => { modal = document.getElementById('my_modal_1') as HTMLDialogElement; if (modal) { modal.showModal(); } + + // 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); + } }); function close() { @@ -88,6 +136,12 @@ } } + // Update UTC dates when local dates change + function updateUTCDates() { + utcStartDate = localStartDate ? toUTCDatetime(localStartDate) : null; + utcEndDate = localEndDate ? toUTCDatetime(localEndDate) : null; + } + async function geocode(e: Event | null) { if (e) { e.preventDefault(); @@ -172,47 +226,56 @@ Math.round(transportation.destination_longitude * 1e6) / 1e6; } - if (transportation.end_date && !transportation.date) { - transportation.date = null; - transportation.end_date = null; + // Validate dates + if (localEndDate && !localStartDate) { + addToast('error', $t('adventures.start_date_required')); + return; } - if (transportation.date && !transportation.end_date) { - transportation.end_date = transportation.date; + if (localStartDate && !localEndDate) { + // If only start date is provided, set end date to the same value + localEndDate = localStartDate; + utcEndDate = utcStartDate; } if ( - transportation.date && - transportation.end_date && - transportation.date > transportation.end_date + localStartDate && + localEndDate && + DateTime.fromISO(localStartDate).toMillis() > DateTime.fromISO(localEndDate).toMillis() ) { addToast('error', $t('adventures.start_before_end_error')); return; } - // Convert local dates to UTC - if (transportation.date && !transportation.date.includes('Z')) { - transportation.date = new Date(transportation.date).toISOString(); - } - if (transportation.end_date && !transportation.end_date.includes('Z')) { - transportation.end_date = new Date(transportation.end_date).toISOString(); - } + // Use the stored UTC dates for submission + const submissionData = { + ...transportation, + date: utcStartDate, + end_date: utcEndDate + }; if (transportation.type != 'plane') { - transportation.flight_number = ''; + submissionData.flight_number = ''; } - if (transportation.id === '') { + if (submissionData.id === '') { let res = await fetch('/api/transportations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(transportation) + body: JSON.stringify(submissionData) }); let data = await res.json(); if (data.id) { transportation = data as Transportation; + // 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); + addToast('success', $t('adventures.adventure_created')); dispatch('save', transportation); } else { @@ -225,11 +288,18 @@ headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(transportation) + body: JSON.stringify(submissionData) }); let data = await res.json(); if (data.id) { transportation = data as Transportation; + // 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); + addToast('success', $t('adventures.adventure_updated')); dispatch('save', transportation); } else { @@ -385,6 +455,7 @@ {$t('adventures.date_information')}
+
- {#if transportation.date} + {#if localStartDate}
@@ -451,13 +524,17 @@ {$t('lodging.current_timezone')}: - {(() => { - const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone; - const [continent, city] = tz.split('/'); - return `${continent} (${city.replace('_', ' ')})`; - })()} + {selectedTimezone} + {#if utcStartDate} +
+ UTC Time: {DateTime.fromISO(utcStartDate).toISO().slice(0, 16).replace('T', ' ')} + {#if utcEndDate && utcEndDate !== utcStartDate} + to {DateTime.fromISO(utcEndDate).toISO().slice(0, 16).replace('T', ' ')} + {/if} +
+ {/if} @@ -585,11 +662,6 @@ class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg" standardControls > - - - {#if transportation.origin_latitude && transportation.origin_longitude} /> {/if} - {#if transportation.from_location || transportation.to_location}