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 @@
+
+
+
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}