diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 20dd39f..add0ef4 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -92,6 +92,7 @@ import Crown from '~icons/mdi/crown'; import AttachmentCard from './AttachmentCard.svelte'; import LocationDropdown from './LocationDropdown.svelte'; + import DateRangeCollapse from './DateRangeCollapse.svelte'; let modal: HTMLDialogElement; let wikiError: string = ''; @@ -684,237 +685,8 @@ -
- -
- {$t('adventures.visits')} ({adventure.visits.length}) -
-
- -
- {#if !allDay} - { - if (e.key === 'Enter') { - e.preventDefault(); - addNewVisit(); - } - }} - /> - { - if (e.key === 'Enter') { - e.preventDefault(); - addNewVisit(); - } - }} - /> - {:else} - { - if (e.key === 'Enter') { - e.preventDefault(); - addNewVisit(); - } - }} - /> - { - if (e.key === 'Enter') { - e.preventDefault(); - addNewVisit(); - } - }} - /> - {/if} -
-
- - -
- {#if !allDay} - - {/if} -
- -
- - {#if adventure.visits.length > 0} -

{$t('adventures.my_visits')}

- {#each adventure.visits as visit} -
-
-

- {#if isAllDay(visit.start_date)} - - {new Date(visit.start_date).toLocaleDateString(undefined, { - timeZone: 'UTC' - })} - {:else} - - {new Date(visit.start_date).toLocaleDateString()} ({new Date( - visit.start_date - ).toLocaleTimeString()}) - {/if} -

- {#if visit.end_date && visit.end_date !== visit.start_date} -

- {#if isAllDay(visit.end_date)} - - {new Date(visit.end_date).toLocaleDateString(undefined, { - timeZone: 'UTC' - })} - {:else} - - {new Date(visit.end_date).toLocaleDateString()} ({new Date( - visit.end_date - ).toLocaleTimeString()}) - {/if} -

- {/if} -
- - -
-
-

{visit.notes}

-
- {/each} - {/if} -
-
+
diff --git a/frontend/src/lib/components/DateRangeCollapse.svelte b/frontend/src/lib/components/DateRangeCollapse.svelte new file mode 100644 index 0000000..d8fc699 --- /dev/null +++ b/frontend/src/lib/components/DateRangeCollapse.svelte @@ -0,0 +1,387 @@ + + +
+ +
+ {$t('adventures.date_information')} +
+
+ +
+ +
+ + {$t('adventures.all_day')} + { + // clear local dates when toggling all day + if (allDay) { + localStartDate = localStartDate.split('T')[0]; + localEndDate = localEndDate.split('T')[0]; + } else { + localStartDate = localStartDate + 'T00:00'; + localEndDate = localEndDate + 'T23:59'; + } + // Update UTC dates when toggling all day + utcStartDate = updateUTCDate({ + localDate: localStartDate, + timezone: selectedTimezone, + allDay + }).utcDate; + utcEndDate = updateUTCDate({ + localDate: localEndDate, + timezone: selectedTimezone, + allDay + }).utcDate; + // Update local dates when toggling all day + localStartDate = updateLocalDate({ + utcDate: utcStartDate, + timezone: selectedTimezone + }).localDate; + localEndDate = updateLocalDate({ + utcDate: utcEndDate, + timezone: selectedTimezone + }).localDate; + }} + /> + + + +
+ +
+ + + {#if allDay} + + {:else} + + {/if} + + {#if collection && collection.start_date && collection.end_date} + + {/if} +
+ + + {#if localStartDate} +
+ + + {#if allDay} + + {:else} + + {/if} +
+ {/if} + + + {#if type === 'adventure'} +
+ + +
+ {/if} +
+ + + {#if !validateDateRange(localStartDate, localEndDate).valid} + + {/if} + + {#if visits && visits.length > 0} +
+ {#each visits as visit} +
+

+ {#if isAllDay(visit.start_date)} + All Day + {visit.start_date.split('T')[0]} – {visit.end_date.split('T')[0]} + {:else} + {new Date(visit.start_date).toLocaleString()} – {new Date( + visit.end_date + ).toLocaleString()} + {/if} +

+ + + + {#if visit.notes} +

+ "{visit.notes}" +

+ {/if} + +
+ + + +
+
+ {/each} +
+ {/if} +
+ + {#if type === 'adventure'} + + {/if} +
+
+
diff --git a/frontend/src/lib/components/DateRangeDropdown.svelte b/frontend/src/lib/components/DateRangeDropdown.svelte deleted file mode 100644 index 608d8bd..0000000 --- a/frontend/src/lib/components/DateRangeDropdown.svelte +++ /dev/null @@ -1,172 +0,0 @@ - - -
- -
- {$t('adventures.date_information')} -
-
- -
- -
- - -
- -
- - - - - {#if collection && collection.start_date && collection.end_date} - - {/if} -
- - - {#if localStartDate} -
- - - -
- {/if} -
- - - {#if !validateDateRange(localStartDate, localEndDate).valid} - - {/if} - - -
-
diff --git a/frontend/src/lib/components/LodgingModal.svelte b/frontend/src/lib/components/LodgingModal.svelte index 9a2975e..cbe05c8 100644 --- a/frontend/src/lib/components/LodgingModal.svelte +++ b/frontend/src/lib/components/LodgingModal.svelte @@ -5,6 +5,7 @@ import MarkdownEditor from './MarkdownEditor.svelte'; import type { Collection, Lodging } from '$lib/types'; import LocationDropdown from './LocationDropdown.svelte'; + import DateRangeCollapse from './DateRangeCollapse.svelte'; const dispatch = createEventDispatcher(); @@ -12,22 +13,10 @@ export let lodgingToEdit: Lodging | null = null; let modal: HTMLDialogElement; - let constrainDates: boolean = false; let lodging: Lodging = { ...initializeLodging(lodgingToEdit) }; let fullStartDate: string = ''; let fullEndDate: string = ''; - // 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); - } - type LodgingType = { value: string; label: string; @@ -47,27 +36,27 @@ { value: 'other', label: 'Other' } ]; - // Initialize hotel with values from hotelToEdit or default values - function initializeLodging(hotelToEdit: Lodging | null): Lodging { + // Initialize hotel with values from lodgingToEdit or default values + function initializeLodging(lodgingToEdit: Lodging | null): Lodging { return { - id: hotelToEdit?.id || '', - user_id: hotelToEdit?.user_id || '', - name: hotelToEdit?.name || '', - type: hotelToEdit?.type || 'other', - description: hotelToEdit?.description || '', - rating: hotelToEdit?.rating || NaN, - link: hotelToEdit?.link || '', - check_in: hotelToEdit?.check_in ? toLocalDatetime(hotelToEdit.check_in) : null, - check_out: hotelToEdit?.check_out ? toLocalDatetime(hotelToEdit.check_out) : null, - reservation_number: hotelToEdit?.reservation_number || '', - price: hotelToEdit?.price || null, - latitude: hotelToEdit?.latitude || null, - longitude: hotelToEdit?.longitude || null, - location: hotelToEdit?.location || '', - is_public: hotelToEdit?.is_public || false, - collection: hotelToEdit?.collection || collection.id, - created_at: hotelToEdit?.created_at || '', - updated_at: hotelToEdit?.updated_at || '' + id: lodgingToEdit?.id || '', + user_id: lodgingToEdit?.user_id || '', + name: lodgingToEdit?.name || '', + type: lodgingToEdit?.type || 'other', + description: lodgingToEdit?.description || '', + rating: lodgingToEdit?.rating || NaN, + link: lodgingToEdit?.link || '', + check_in: lodgingToEdit?.check_in || null, + check_out: lodgingToEdit?.check_out || null, + reservation_number: lodgingToEdit?.reservation_number || '', + price: lodgingToEdit?.price || null, + latitude: lodgingToEdit?.latitude || null, + longitude: lodgingToEdit?.longitude || null, + location: lodgingToEdit?.location || '', + is_public: lodgingToEdit?.is_public || false, + collection: lodgingToEdit?.collection || collection.id, + created_at: lodgingToEdit?.created_at || '', + updated_at: lodgingToEdit?.updated_at || '' }; } @@ -104,27 +93,6 @@ async function handleSubmit(event: Event) { event.preventDefault(); - if (lodging.check_in && !lodging.check_out) { - const checkInDate = new Date(lodging.check_in); - checkInDate.setDate(checkInDate.getDate() + 1); - lodging.check_out = checkInDate.toISOString(); - } - - if (lodging.check_in && lodging.check_out && lodging.check_in > lodging.check_out) { - addToast('error', $t('adventures.start_before_end_error')); - return; - } - - // Only convert to UTC if the time is still in local format. - if (lodging.check_in && !lodging.check_in.includes('Z')) { - // new Date(lodging.check_in) interprets the input as local time. - lodging.check_in = new Date(lodging.check_in).toISOString(); - } - if (lodging.check_out && !lodging.check_out.includes('Z')) { - lodging.check_out = new Date(lodging.check_out).toISOString(); - } - console.log(lodging.check_in, lodging.check_out); - // Create or update lodging... const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`; const method = lodging.id === '' ? 'POST' : 'PATCH'; @@ -331,85 +299,11 @@
-
- -
- {$t('adventures.date_information')} -
-
- -
- - - {#if collection && collection.start_date && collection.end_date} - {/if} -
- -
-
- -
- -
- -
-
- -
-
+ diff --git a/frontend/src/lib/components/TimezoneSelector.svelte b/frontend/src/lib/components/TimezoneSelector.svelte index c0738b8..fe3bf36 100644 --- a/frontend/src/lib/components/TimezoneSelector.svelte +++ b/frontend/src/lib/components/TimezoneSelector.svelte @@ -6,6 +6,7 @@ let dropdownOpen = false; let searchQuery = ''; + let searchInput: HTMLInputElement; const timezones = Intl.supportedValuesOf('timeZone'); // Filter timezones based on search query @@ -19,6 +20,23 @@ searchQuery = ''; } + // Focus search input when dropdown opens + $: if (dropdownOpen && searchInput) { + // Use setTimeout to delay focus until after the element is rendered + setTimeout(() => searchInput.focus(), 0); + } + + function handleKeydown(event: KeyboardEvent, tz?: string) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + if (tz) selectTimezone(tz); + else dropdownOpen = !dropdownOpen; + } else if (event.key === 'Escape') { + event.preventDefault(); + dropdownOpen = false; + } + } + // Close dropdown if clicked outside onMount(() => { const handleClickOutside = (e: MouseEvent) => { @@ -31,16 +49,20 @@
-
-
- -
- {$t('adventures.date_information')} -
-
- - -
- - {#if collection && collection.start_date && collection.end_date} - {/if} -
- -
-
- - {#if localStartDate} -
- -
- -
-
- {/if} - - {#if utcStartDate} -
- UTC Time: {formatUTCDate(utcStartDate)} - {#if utcEndDate && utcEndDate !== utcStartDate} - to {formatUTCDate(utcEndDate)} - {/if} -
- {/if} -
-
+ + -
diff --git a/frontend/src/lib/dateUtils.ts b/frontend/src/lib/dateUtils.ts index d47f119..3a4a18c 100644 --- a/frontend/src/lib/dateUtils.ts +++ b/frontend/src/lib/dateUtils.ts @@ -26,10 +26,22 @@ export function toLocalDatetime( */ export function toUTCDatetime( localDate: string, - timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone + timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone, + allDay: boolean = false ): string | null { if (!localDate) return null; - return DateTime.fromISO(localDate, { zone: timezone }).toUTC().toISO(); + + if (allDay) { + // Treat input as date-only, set UTC midnight manually + return DateTime.fromISO(localDate, { zone: 'UTC' }) + .startOf('day') + .toISO({ suppressMilliseconds: true }); + } + + // Normal timezone conversion for datetime-local input + return DateTime.fromISO(localDate, { zone: timezone }) + .toUTC() + .toISO({ suppressMilliseconds: true }); } /** @@ -54,9 +66,17 @@ export function updateLocalDate({ * @param params Object containing local date and timezone * @returns Object with updated UTC datetime string */ -export function updateUTCDate({ localDate, timezone }: { localDate: string; timezone: string }) { +export function updateUTCDate({ + localDate, + timezone, + allDay = false +}: { + localDate: string; + timezone: string; + allDay?: boolean; +}) { return { - utcDate: toUTCDatetime(localDate, timezone) + utcDate: toUTCDatetime(localDate, timezone, allDay) }; } diff --git a/frontend/src/routes/datetest/+page.svelte b/frontend/src/routes/datetest/+page.svelte deleted file mode 100644 index b06ba1f..0000000 --- a/frontend/src/routes/datetest/+page.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - -

{new Date(utcStartDate).toLocaleString()} - {new Date(utcEndDate).toLocaleString()}

- -

UTC Start Date: {utcStartDate}

-

UTC End Date: {utcEndDate}