1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-22 22:39:36 +02:00

Refactor date handling components: Replace DateRangeDropdown with DateRangeCollapse

- Introduced DateRangeCollapse.svelte to manage date range selection with timezone support.
- Removed DateRangeDropdown.svelte as it was redundant.
- Updated LodgingModal and TransportationModal to utilize DateRangeCollapse for date selection.
- Enhanced date conversion utilities to handle all-day events correctly.
- Adjusted TimezoneSelector for improved accessibility and focus management.
- Updated date handling logic in dateUtils.ts to support all-day events.
- Modified test page to reflect changes in date range component usage.
This commit is contained in:
Sean Morley 2025-05-09 10:24:29 -04:00
parent 827b150965
commit 2c50ca0b1a
8 changed files with 484 additions and 758 deletions

View file

@ -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 @@
<ActivityComplete bind:activities={adventure.activity_types} />
</div>
</div>
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">
{$t('adventures.visits')} ({adventure.visits.length})
</div>
<div class="collapse-content">
<label class="label cursor-pointer flex items-start space-x-2">
{#if adventure.collection && collection && collection.start_date && collection.end_date}
<span class="label-text">{$t('adventures.date_constrain')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
on:change={() => (constrainDates = !constrainDates)}
/>
{/if}
<span class="label-text">{$t('adventures.all_day')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
bind:checked={allDay}
/>
</label>
<div class="flex gap-2 mb-1">
{#if !allDay}
<input
type="datetime-local"
class="input input-bordered w-full"
placeholder={$t('adventures.start_date')}
min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''}
bind:value={new_start_date}
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addNewVisit();
}
}}
/>
<input
type="datetime-local"
class="input input-bordered w-full"
placeholder={$t('adventures.end_date')}
bind:value={new_end_date}
min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''}
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addNewVisit();
}
}}
/>
{:else}
<input
type="date"
class="input input-bordered w-full"
placeholder={$t('adventures.start_date')}
min={constrainDates ? fullStartDateOnly : ''}
max={constrainDates ? fullEndDateOnly : ''}
bind:value={new_start_date}
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addNewVisit();
}
}}
/>
<input
type="date"
class="input input-bordered w-full"
placeholder={$t('adventures.end_date')}
bind:value={new_end_date}
min={constrainDates ? fullStartDateOnly : ''}
max={constrainDates ? fullEndDateOnly : ''}
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addNewVisit();
}
}}
/>
{/if}
</div>
<div class="flex gap-2 mb-1">
<!-- textarea for notes -->
<textarea
class="textarea textarea-bordered w-full"
placeholder={$t('adventures.add_notes')}
bind:value={new_notes}
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addNewVisit();
}
}}
></textarea>
</div>
{#if !allDay}
<div role="alert" class="alert shadow-lg bg-neutral mt-2 mb-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info h-6 w-6 shrink-0"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>
{$t('lodging.current_timezone')}:
{(() => {
const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
const [continent, city] = tz.split('/');
return `${continent} (${city.replace('_', ' ')})`;
})()}
</span>
</div>
{/if}
<div class="flex gap-2">
<button type="button" class="btn btn-neutral" on:click={addNewVisit}
>{$t('adventures.add')}</button
>
</div>
{#if adventure.visits.length > 0}
<h2 class="font-bold text-xl mt-2">{$t('adventures.my_visits')}</h2>
{#each adventure.visits as visit}
<div class="flex flex-col gap-2">
<div class="flex gap-2 items-center">
<p>
{#if isAllDay(visit.start_date)}
<!-- For all-day events, show just the date -->
{new Date(visit.start_date).toLocaleDateString(undefined, {
timeZone: 'UTC'
})}
{:else}
<!-- For timed events, show date and time -->
{new Date(visit.start_date).toLocaleDateString()} ({new Date(
visit.start_date
).toLocaleTimeString()})
{/if}
</p>
{#if visit.end_date && visit.end_date !== visit.start_date}
<p>
{#if isAllDay(visit.end_date)}
<!-- For all-day events, show just the date -->
{new Date(visit.end_date).toLocaleDateString(undefined, {
timeZone: 'UTC'
})}
{:else}
<!-- For timed events, show date and time -->
{new Date(visit.end_date).toLocaleDateString()} ({new Date(
visit.end_date
).toLocaleTimeString()})
{/if}
</p>
{/if}
<div>
<button
type="button"
class="btn btn-sm btn-neutral"
on:click={() => {
// Determine if this is an all-day event
const isAllDayEvent = isAllDay(visit.start_date);
allDay = isAllDayEvent;
if (isAllDayEvent) {
// For all-day events, use date only
new_start_date = visit.start_date.split('T')[0];
new_end_date = visit.end_date.split('T')[0];
} else {
// For timed events, format properly for datetime-local input
const startDate = new Date(visit.start_date);
const endDate = new Date(visit.end_date);
// Format as yyyy-MM-ddThh:mm
new_start_date =
startDate.getFullYear() +
'-' +
String(startDate.getMonth() + 1).padStart(2, '0') +
'-' +
String(startDate.getDate()).padStart(2, '0') +
'T' +
String(startDate.getHours()).padStart(2, '0') +
':' +
String(startDate.getMinutes()).padStart(2, '0');
new_end_date =
endDate.getFullYear() +
'-' +
String(endDate.getMonth() + 1).padStart(2, '0') +
'-' +
String(endDate.getDate()).padStart(2, '0') +
'T' +
String(endDate.getHours()).padStart(2, '0') +
':' +
String(endDate.getMinutes()).padStart(2, '0');
}
new_notes = visit.notes;
adventure.visits = adventure.visits.filter((v) => v !== visit);
}}
>
{$t('lodging.edit')}
</button>
<button
type="button"
class="btn btn-sm btn-error"
on:click={() => {
adventure.visits = adventure.visits.filter((v) => v !== visit);
}}
>
{$t('adventures.remove')}
</button>
</div>
</div>
<p class="whitespace-pre-wrap -mt-2 mb-2">{visit.notes}</p>
</div>
{/each}
{/if}
</div>
</div>
<DateRangeCollapse type="adventure" {collection} bind:visits={adventure.visits} />
<div>
<div class="mt-4">

View file

@ -0,0 +1,387 @@
<script lang="ts">
import type { Collection } from '$lib/types';
import TimezoneSelector from './TimezoneSelector.svelte';
import { t } from 'svelte-i18n';
export let collection: Collection | null = null;
import { updateLocalDate, updateUTCDate, validateDateRange, formatUTCDate } from '$lib/dateUtils';
import { onMount } from 'svelte';
import { isAllDay } from '$lib';
export let type: 'adventure' | 'transportation' | 'lodging' = 'adventure';
// Initialize with browser's timezone
let selectedTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
let allDay: boolean = false;
// Store the UTC dates as source of truth
export let utcStartDate: string | null = null;
export let utcEndDate: string | null = null;
console.log('UTC Start Date:', utcStartDate);
console.log('UTC End Date:', utcEndDate);
export let note: string | null = null;
type Visit = {
id: string;
start_date: string;
end_date: string;
notes: string;
};
export let visits: Visit[] | null = null;
// Local display values
let localStartDate: string = '';
let localEndDate: string = '';
let fullStartDate: string = '';
let fullEndDate: string = '';
let constrainDates: boolean = false;
let isEditing = false; // Disable reactivity when editing
onMount(async () => {
console.log('Selected timezone:', selectedTimezone);
console.log('UTC Start Date:', utcStartDate);
console.log('UTC End Date:', utcEndDate);
// Initialize UTC dates from transportationToEdit if available
localStartDate = updateLocalDate({
utcDate: utcStartDate,
timezone: selectedTimezone
}).localDate;
localEndDate = updateLocalDate({
utcDate: utcEndDate,
timezone: selectedTimezone
}).localDate;
});
if (collection && collection.start_date && collection.end_date) {
fullStartDate = `${collection.start_date}T00:00`;
fullEndDate = `${collection.end_date}T23:59`;
}
// Update local display dates whenever timezone or UTC dates change
$: if (!isEditing) {
if (allDay) {
localStartDate = utcStartDate?.substring(0, 10) ?? '';
localEndDate = utcEndDate?.substring(0, 10) ?? '';
} else {
const start = updateLocalDate({
utcDate: utcStartDate,
timezone: selectedTimezone
}).localDate;
const end = updateLocalDate({
utcDate: utcEndDate,
timezone: selectedTimezone
}).localDate;
localStartDate = start;
localEndDate = end;
}
}
// Update UTC dates when local dates change
function handleLocalDateChange() {
utcStartDate = updateUTCDate({
localDate: localStartDate,
timezone: selectedTimezone,
allDay
}).utcDate;
utcEndDate = updateUTCDate({
localDate: localEndDate,
timezone: selectedTimezone,
allDay
}).utcDate;
}
</script>
<div class="collapse collapse-plus bg-base-200 mb-4 rounded-lg">
<input type="checkbox" />
<div class="collapse-title text-xl font-semibold">
{$t('adventures.date_information')}
</div>
<div class="collapse-content space-y-6">
<!-- Timezone Selector -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<TimezoneSelector bind:selectedTimezone />
</div>
<span class="label-text">{$t('adventures.all_day')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
bind:checked={allDay}
on:change={() => {
// 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;
}}
/>
<!-- All Day Event Checkbox -->
<!-- Dates Input Section -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Start Date -->
<div class="flex flex-col space-y-2">
<label for="date" class="font-medium">
{$t('adventures.start_date')}
</label>
{#if allDay}
<input
type="date"
id="date"
name="date"
bind:value={localStartDate}
on:change={handleLocalDateChange}
min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full"
/>
{:else}
<input
type="datetime-local"
id="date"
name="date"
bind:value={localStartDate}
on:change={handleLocalDateChange}
min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full"
/>
{/if}
{#if collection && collection.start_date && collection.end_date}
<label class="flex items-center gap-2 mt-2">
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
on:change={() => (constrainDates = !constrainDates)}
/>
<span class="label-text">{$t('adventures.date_constrain')}</span>
</label>
{/if}
</div>
<!-- End Date -->
{#if localStartDate}
<div class="flex flex-col space-y-2">
<label for="end_date" class="font-medium">
{$t('adventures.end_date')}
</label>
{#if allDay}
<input
type="date"
id="end_date"
name="end_date"
bind:value={localEndDate}
on:change={handleLocalDateChange}
min={constrainDates ? localStartDate : ''}
max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full"
/>
{:else}
<input
type="datetime-local"
id="end_date"
name="end_date"
bind:value={localEndDate}
on:change={handleLocalDateChange}
min={constrainDates ? localStartDate : ''}
max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full"
/>
{/if}
</div>
{/if}
<!-- Notes -->
{#if type === 'adventure'}
<div class="flex gap-2 mb-1">
<!-- textarea for notes -->
<textarea
class="textarea textarea-bordered w-full"
placeholder={$t('adventures.add_notes')}
bind:value={note}
></textarea>
</div>
{/if}
</div>
<!-- Validation Message -->
{#if !validateDateRange(localStartDate, localEndDate).valid}
<div role="alert" class="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{$t('adventures.invalid_date_range')}</span>
</div>
{/if}
{#if visits && visits.length > 0}
<div class="space-y-4">
{#each visits as visit}
<div
class="p-4 border border-neutral rounded-lg bg-base-100 shadow-sm flex flex-col gap-2"
>
<p class="text-sm text-base-content font-medium">
{#if isAllDay(visit.start_date)}
<span class="badge badge-outline mr-2">All Day</span>
{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}
</p>
<!-- If the selected timezone is not the current one show the timezone + the time converted there -->
{#if visit.notes}
<p class="text-sm text-base-content opacity-70 italic">
"{visit.notes}"
</p>
{/if}
<div class="flex gap-2 mt-2">
<button
class="btn btn-error btn-sm"
type="button"
on:click={() => {
if (visits) {
visits = visits.filter((v) => v.id !== visit.id);
}
}}
>
{$t('adventures.remove')}
</button>
<button
class="btn btn-primary btn-sm"
type="button"
on:click={() => {
isEditing = true;
const isAllDayEvent = isAllDay(visit.start_date);
allDay = isAllDayEvent;
if (isAllDayEvent) {
localStartDate = visit.start_date.split('T')[0];
localEndDate = visit.end_date.split('T')[0];
} else {
const startDate = new Date(visit.start_date);
const endDate = new Date(visit.end_date);
localStartDate = `${startDate.getFullYear()}-${String(
startDate.getMonth() + 1
).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}T${String(
startDate.getHours()
).padStart(2, '0')}:${String(startDate.getMinutes()).padStart(2, '0')}`;
localEndDate = `${endDate.getFullYear()}-${String(
endDate.getMonth() + 1
).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}T${String(
endDate.getHours()
).padStart(2, '0')}:${String(endDate.getMinutes()).padStart(2, '0')}`;
}
// remove it from visits
if (visits) {
visits = visits.filter((v) => v.id !== visit.id);
}
note = visit.notes;
constrainDates = true;
utcStartDate = visit.start_date;
utcEndDate = visit.end_date;
type = 'adventure';
setTimeout(() => {
isEditing = false;
}, 0);
}}
>
{$t('lodging.edit')}
</button>
</div>
</div>
{/each}
</div>
{/if}
<div class="flex gap-2 mb-1">
<!-- add button -->
{#if type === 'adventure'}
<button
class="btn btn-primary"
type="button"
on:click={() => {
const newVisit = {
id: crypto.randomUUID(),
start_date: utcStartDate ?? '',
end_date: utcEndDate ?? utcStartDate ?? '',
notes: note ?? ''
};
// Ensure reactivity by assigning a *new* array
if (visits) {
visits = [...visits, newVisit];
} else {
visits = [newVisit];
}
// Optionally clear the form
note = '';
localStartDate = '';
localEndDate = '';
utcStartDate = null;
utcEndDate = null;
}}
>
{$t('adventures.add')}
</button>
{/if}
</div>
</div>
</div>

View file

@ -1,172 +0,0 @@
<script lang="ts">
import type { Collection } from '$lib/types';
import TimezoneSelector from './TimezoneSelector.svelte';
import { t } from 'svelte-i18n';
export let collection: Collection | null = null;
import { updateLocalDate, updateUTCDate, validateDateRange, formatUTCDate } from '$lib/dateUtils';
import { onMount } from 'svelte';
// Initialize with browser's timezone
let selectedTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Store the UTC dates as source of truth
export let utcStartDate: string | null = null;
export let utcEndDate: string | null = null;
// Local display values
let localStartDate: string = '';
let localEndDate: string = '';
let fullStartDate: string = '';
let fullEndDate: string = '';
let constrainDates: boolean = false;
onMount(async () => {
// Initialize UTC dates from transportationToEdit if available
localStartDate = updateLocalDate({
utcDate: utcStartDate,
timezone: selectedTimezone
}).localDate;
localEndDate = updateLocalDate({
utcDate: utcEndDate,
timezone: selectedTimezone
}).localDate;
});
if (collection && collection.start_date && collection.end_date) {
fullStartDate = `${collection.start_date}T00:00`;
fullEndDate = `${collection.end_date}T23:59`;
}
// Update local display dates whenever timezone or UTC dates change
$: {
localStartDate = updateLocalDate({
utcDate: utcStartDate,
timezone: selectedTimezone
}).localDate;
localEndDate = updateLocalDate({
utcDate: utcEndDate,
timezone: selectedTimezone
}).localDate;
}
// Update UTC dates when local dates change
function handleLocalDateChange() {
utcStartDate = updateUTCDate({
localDate: localStartDate,
timezone: selectedTimezone
}).utcDate;
utcEndDate = updateUTCDate({
localDate: localEndDate,
timezone: selectedTimezone
}).utcDate;
}
</script>
<div class="collapse collapse-plus bg-base-200 mb-4 rounded-lg">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-semibold">
{$t('adventures.date_information')}
</div>
<div class="collapse-content space-y-6">
<!-- Timezone Selector -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<TimezoneSelector bind:selectedTimezone />
</div>
<!-- Dates Input Section -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Start Date -->
<div class="flex flex-col space-y-2">
<label for="date" class="font-medium">
{$t('adventures.start_date')}
</label>
<input
type="datetime-local"
id="date"
name="date"
bind:value={localStartDate}
on:change={handleLocalDateChange}
min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full"
/>
{#if collection && collection.start_date && collection.end_date}
<label class="flex items-center gap-2 mt-2">
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
on:change={() => (constrainDates = !constrainDates)}
/>
<span class="label-text">{$t('adventures.date_constrain')}</span>
</label>
{/if}
</div>
<!-- End Date -->
{#if localStartDate}
<div class="flex flex-col space-y-2">
<label for="end_date" class="font-medium">
{$t('adventures.end_date')}
</label>
<input
type="datetime-local"
id="end_date"
name="end_date"
bind:value={localEndDate}
on:change={handleLocalDateChange}
min={constrainDates ? localStartDate : ''}
max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full"
/>
</div>
{/if}
</div>
<!-- Validation Message -->
{#if !validateDateRange(localStartDate, localEndDate).valid}
<div role="alert" class="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{$t('adventures.invalid_date_range')}</span>
</div>
{/if}
<!--
<div role="alert" class="alert shadow-lg bg-neutral text-neutral-content mt-6">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info h-6 w-6 shrink-0"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span class="ml-2">
{$t('lodging.current_timezone')}: {selectedTimezone}
</span>
</div> -->
</div>
</div>

View file

@ -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 @@
</div>
</div>
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.date_information')}
</div>
<div class="collapse-content">
<!-- Check In -->
<div>
<label for="date">
{$t('lodging.check_in')}
</label>
{#if collection && collection.start_date && collection.end_date}<label
class="label cursor-pointer flex items-start space-x-2"
>
<span class="label-text">{$t('adventures.date_constrain')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
on:change={() => (constrainDates = !constrainDates)}
/></label
>
{/if}
<div>
<input
type="datetime-local"
id="date"
name="date"
bind:value={lodging.check_in}
min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
<!-- End Date -->
<div>
<label for="end_date">
{$t('lodging.check_out')}
</label>
<div>
<input
type="datetime-local"
id="end_date"
name="end_date"
min={constrainDates ? lodging.check_in : ''}
max={constrainDates ? fullEndDate : ''}
bind:value={lodging.check_out}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
<div role="alert" class="alert shadow-lg bg-neutral mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info h-6 w-6 shrink-0"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>
{$t('lodging.current_timezone')}:
{(() => {
const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
const [continent, city] = tz.split('/');
return `${continent} (${city.replace('_', ' ')})`;
})()}
</span>
</div>
</div>
</div>
<DateRangeCollapse
type="lodging"
bind:utcStartDate={lodging.check_in}
bind:utcEndDate={lodging.check_out}
/>
<!-- Location Information -->
<LocationDropdown bind:item={lodging} />

View file

@ -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 @@
</script>
<div class="form-control w-full max-w-xs relative" id="tz-selector">
<label class="label">
<label class="label" for="timezone-display">
<span class="label-text">Timezone</span>
</label>
<!-- Trigger -->
<div
id="timezone-display"
tabindex="0"
role="button"
aria-haspopup="listbox"
aria-expanded={dropdownOpen}
class="input input-bordered flex justify-between items-center cursor-pointer"
on:click={() => (dropdownOpen = !dropdownOpen)}
on:keydown={handleKeydown}
>
<span class="truncate">{selectedTimezone}</span>
<svg
@ -49,6 +71,7 @@
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
@ -58,6 +81,8 @@
{#if dropdownOpen}
<div
class="absolute mt-1 z-10 bg-base-100 shadow-lg rounded-box w-full max-h-60 overflow-y-auto"
role="listbox"
aria-labelledby="timezone-display"
>
<!-- Search -->
<div class="sticky top-0 bg-base-100 p-2 border-b">
@ -66,7 +91,7 @@
placeholder="Search timezone"
class="input input-sm input-bordered w-full"
bind:value={searchQuery}
autofocus
bind:this={searchInput}
/>
</div>
@ -75,12 +100,16 @@
<ul class="menu p-2 space-y-1">
{#each filteredTimezones as tz}
<li>
<a
class={`truncate ${tz === selectedTimezone ? 'active font-bold' : ''}`}
on:click|preventDefault={() => selectTimezone(tz)}
<button
type="button"
class={`w-full text-left truncate ${tz === selectedTimezone ? 'active font-bold' : ''}`}
on:click={() => selectTimezone(tz)}
on:keydown={(e) => handleKeydown(e, tz)}
role="option"
aria-selected={tz === selectedTimezone}
>
{tz}
</a>
</button>
</li>
{/each}
</ul>

View file

@ -6,37 +6,23 @@
import { addToast } from '$lib/toasts';
let modal: HTMLDialogElement;
import { t } from 'svelte-i18n';
import { updateLocalDate, updateUTCDate, validateDateRange, formatUTCDate } from '$lib/dateUtils';
// 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';
import DateRangeCollapse from './DateRangeCollapse.svelte';
export let collection: Collection;
export let transportationToEdit: Transportation | null = null;
let constrainDates: boolean = false;
// Initialize transportation object
let transportation: Transportation = {
id: transportationToEdit?.id || '',
type: transportationToEdit?.type || '',
name: transportationToEdit?.name || '',
description: transportationToEdit?.description || '',
date: null,
end_date: null,
date: transportationToEdit?.date || null,
end_date: transportationToEdit?.end_date || null,
rating: transportationToEdit?.rating || 0,
link: transportationToEdit?.link || '',
flight_number: transportationToEdit?.flight_number || '',
@ -53,58 +39,20 @@
destination_longitude: transportationToEdit?.destination_longitude || NaN
};
let fullStartDate: string = '';
let fullEndDate: string = '';
let starting_airport: string = '';
let ending_airport: string = '';
if (collection.start_date && collection.end_date) {
fullStartDate = `${collection.start_date}T00:00`;
fullEndDate = `${collection.end_date}T23:59`;
}
$: {
if (!transportation.rating) {
transportation.rating = NaN;
}
}
// Update local display dates whenever timezone or UTC dates change
$: {
localStartDate = updateLocalDate({
utcDate: utcStartDate,
timezone: selectedTimezone
}).localDate;
localEndDate = updateLocalDate({
utcDate: utcEndDate,
timezone: selectedTimezone
}).localDate;
}
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;
}
if (transportationToEdit?.end_date) {
utcEndDate = transportationToEdit.end_date;
}
localStartDate = updateLocalDate({
utcDate: utcStartDate,
timezone: selectedTimezone
}).localDate;
localEndDate = updateLocalDate({
utcDate: utcEndDate,
timezone: selectedTimezone
}).localDate;
});
function close() {
@ -117,18 +65,6 @@
}
}
// Update UTC dates when local dates change
function handleLocalDateChange() {
utcStartDate = updateUTCDate({
localDate: localStartDate,
timezone: selectedTimezone
}).utcDate;
utcEndDate = updateUTCDate({
localDate: localEndDate,
timezone: selectedTimezone
}).utcDate;
}
async function geocode(e: Event | null) {
// Geocoding logic unchanged
if (e) {
@ -214,30 +150,9 @@
Math.round(transportation.destination_longitude * 1e6) / 1e6;
}
// Validate dates using utility function
if (localEndDate && !localStartDate) {
addToast('error', $t('adventures.start_date_required'));
return;
}
if (localStartDate && !localEndDate) {
// If only start date is provided, set end date to the same value
localEndDate = localStartDate;
utcEndDate = utcStartDate;
}
// Validate date range
const validation = validateDateRange(localStartDate, localEndDate);
if (!validation.valid) {
addToast('error', $t('adventures.start_before_end_error'));
return;
}
// Use the stored UTC dates for submission
const submissionData = {
...transportation,
date: utcStartDate,
end_date: utcEndDate
...transportation
};
if (transportation.type != 'plane') {
@ -255,18 +170,6 @@
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;
localStartDate = updateLocalDate({
utcDate: utcStartDate,
timezone: selectedTimezone
}).localDate;
localEndDate = updateLocalDate({
utcDate: utcEndDate,
timezone: selectedTimezone
}).localDate;
addToast('success', $t('adventures.adventure_created'));
dispatch('save', transportation);
@ -285,18 +188,6 @@
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;
localStartDate = updateLocalDate({
utcDate: utcStartDate,
timezone: selectedTimezone
}).localDate;
localEndDate = updateLocalDate({
utcDate: utcEndDate,
timezone: selectedTimezone
}).localDate;
addToast('success', $t('adventures.adventure_updated'));
dispatch('save', transportation);
@ -447,96 +338,14 @@
</div>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.date_information')}
</div>
<div class="collapse-content">
<TimezoneSelector bind:selectedTimezone />
<!-- Start Date -->
<div>
<label for="date">
{$t('adventures.start_date')}
</label>
{#if collection && collection.start_date && collection.end_date}<label
class="label cursor-pointer flex items-start space-x-2"
>
<span class="label-text">{$t('adventures.date_constrain')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
on:change={() => (constrainDates = !constrainDates)}
/></label
>
{/if}
<div>
<input
type="datetime-local"
id="date"
name="date"
bind:value={localStartDate}
on:change={handleLocalDateChange}
min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
<!-- End Date -->
{#if localStartDate}
<div>
<label for="end_date">
{$t('adventures.end_date')}
</label>
<div>
<input
type="datetime-local"
id="end_date"
name="end_date"
min={constrainDates ? localStartDate : ''}
max={constrainDates ? fullEndDate : ''}
bind:value={localEndDate}
on:change={handleLocalDateChange}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
{/if}
<div role="alert" class="alert shadow-lg bg-neutral mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info h-6 w-6 shrink-0"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>
{$t('lodging.current_timezone')}:
{selectedTimezone}
</span>
</div>
{#if utcStartDate}
<div class="text-sm mt-2">
UTC Time: {formatUTCDate(utcStartDate)}
{#if utcEndDate && utcEndDate !== utcStartDate}
to {formatUTCDate(utcEndDate)}
{/if}
</div>
{/if}
</div>
</div>
<DateRangeCollapse
type="transportation"
bind:utcStartDate={transportation.date}
bind:utcEndDate={transportation.end_date}
/>
<!-- Flight Information -->
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">