mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-24 15:29: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:
parent
827b150965
commit
2c50ca0b1a
8 changed files with 484 additions and 758 deletions
|
@ -92,6 +92,7 @@
|
||||||
import Crown from '~icons/mdi/crown';
|
import Crown from '~icons/mdi/crown';
|
||||||
import AttachmentCard from './AttachmentCard.svelte';
|
import AttachmentCard from './AttachmentCard.svelte';
|
||||||
import LocationDropdown from './LocationDropdown.svelte';
|
import LocationDropdown from './LocationDropdown.svelte';
|
||||||
|
import DateRangeCollapse from './DateRangeCollapse.svelte';
|
||||||
let modal: HTMLDialogElement;
|
let modal: HTMLDialogElement;
|
||||||
|
|
||||||
let wikiError: string = '';
|
let wikiError: string = '';
|
||||||
|
@ -684,237 +685,8 @@
|
||||||
<ActivityComplete bind:activities={adventure.activity_types} />
|
<ActivityComplete bind:activities={adventure.activity_types} />
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<DateRangeCollapse type="adventure" {collection} bind:visits={adventure.visits} />
|
||||||
<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>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
|
|
387
frontend/src/lib/components/DateRangeCollapse.svelte
Normal file
387
frontend/src/lib/components/DateRangeCollapse.svelte
Normal 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>
|
|
@ -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>
|
|
|
@ -5,6 +5,7 @@
|
||||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||||
import type { Collection, Lodging } from '$lib/types';
|
import type { Collection, Lodging } from '$lib/types';
|
||||||
import LocationDropdown from './LocationDropdown.svelte';
|
import LocationDropdown from './LocationDropdown.svelte';
|
||||||
|
import DateRangeCollapse from './DateRangeCollapse.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -12,22 +13,10 @@
|
||||||
export let lodgingToEdit: Lodging | null = null;
|
export let lodgingToEdit: Lodging | null = null;
|
||||||
|
|
||||||
let modal: HTMLDialogElement;
|
let modal: HTMLDialogElement;
|
||||||
let constrainDates: boolean = false;
|
|
||||||
let lodging: Lodging = { ...initializeLodging(lodgingToEdit) };
|
let lodging: Lodging = { ...initializeLodging(lodgingToEdit) };
|
||||||
let fullStartDate: string = '';
|
let fullStartDate: string = '';
|
||||||
let fullEndDate: 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 = {
|
type LodgingType = {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -47,27 +36,27 @@
|
||||||
{ value: 'other', label: 'Other' }
|
{ value: 'other', label: 'Other' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Initialize hotel with values from hotelToEdit or default values
|
// Initialize hotel with values from lodgingToEdit or default values
|
||||||
function initializeLodging(hotelToEdit: Lodging | null): Lodging {
|
function initializeLodging(lodgingToEdit: Lodging | null): Lodging {
|
||||||
return {
|
return {
|
||||||
id: hotelToEdit?.id || '',
|
id: lodgingToEdit?.id || '',
|
||||||
user_id: hotelToEdit?.user_id || '',
|
user_id: lodgingToEdit?.user_id || '',
|
||||||
name: hotelToEdit?.name || '',
|
name: lodgingToEdit?.name || '',
|
||||||
type: hotelToEdit?.type || 'other',
|
type: lodgingToEdit?.type || 'other',
|
||||||
description: hotelToEdit?.description || '',
|
description: lodgingToEdit?.description || '',
|
||||||
rating: hotelToEdit?.rating || NaN,
|
rating: lodgingToEdit?.rating || NaN,
|
||||||
link: hotelToEdit?.link || '',
|
link: lodgingToEdit?.link || '',
|
||||||
check_in: hotelToEdit?.check_in ? toLocalDatetime(hotelToEdit.check_in) : null,
|
check_in: lodgingToEdit?.check_in || null,
|
||||||
check_out: hotelToEdit?.check_out ? toLocalDatetime(hotelToEdit.check_out) : null,
|
check_out: lodgingToEdit?.check_out || null,
|
||||||
reservation_number: hotelToEdit?.reservation_number || '',
|
reservation_number: lodgingToEdit?.reservation_number || '',
|
||||||
price: hotelToEdit?.price || null,
|
price: lodgingToEdit?.price || null,
|
||||||
latitude: hotelToEdit?.latitude || null,
|
latitude: lodgingToEdit?.latitude || null,
|
||||||
longitude: hotelToEdit?.longitude || null,
|
longitude: lodgingToEdit?.longitude || null,
|
||||||
location: hotelToEdit?.location || '',
|
location: lodgingToEdit?.location || '',
|
||||||
is_public: hotelToEdit?.is_public || false,
|
is_public: lodgingToEdit?.is_public || false,
|
||||||
collection: hotelToEdit?.collection || collection.id,
|
collection: lodgingToEdit?.collection || collection.id,
|
||||||
created_at: hotelToEdit?.created_at || '',
|
created_at: lodgingToEdit?.created_at || '',
|
||||||
updated_at: hotelToEdit?.updated_at || ''
|
updated_at: lodgingToEdit?.updated_at || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,27 +93,6 @@
|
||||||
async function handleSubmit(event: Event) {
|
async function handleSubmit(event: Event) {
|
||||||
event.preventDefault();
|
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...
|
// Create or update lodging...
|
||||||
const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`;
|
const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`;
|
||||||
const method = lodging.id === '' ? 'POST' : 'PATCH';
|
const method = lodging.id === '' ? 'POST' : 'PATCH';
|
||||||
|
@ -331,85 +299,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
<DateRangeCollapse
|
||||||
<input type="checkbox" checked />
|
type="lodging"
|
||||||
<div class="collapse-title text-xl font-medium">
|
bind:utcStartDate={lodging.check_in}
|
||||||
{$t('adventures.date_information')}
|
bind:utcEndDate={lodging.check_out}
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- Location Information -->
|
<!-- Location Information -->
|
||||||
<LocationDropdown bind:item={lodging} />
|
<LocationDropdown bind:item={lodging} />
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
let dropdownOpen = false;
|
let dropdownOpen = false;
|
||||||
let searchQuery = '';
|
let searchQuery = '';
|
||||||
|
let searchInput: HTMLInputElement;
|
||||||
const timezones = Intl.supportedValuesOf('timeZone');
|
const timezones = Intl.supportedValuesOf('timeZone');
|
||||||
|
|
||||||
// Filter timezones based on search query
|
// Filter timezones based on search query
|
||||||
|
@ -19,6 +20,23 @@
|
||||||
searchQuery = '';
|
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
|
// Close dropdown if clicked outside
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
@ -31,16 +49,20 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="form-control w-full max-w-xs relative" id="tz-selector">
|
<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>
|
<span class="label-text">Timezone</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!-- Trigger -->
|
<!-- Trigger -->
|
||||||
<div
|
<div
|
||||||
|
id="timezone-display"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="button"
|
role="button"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={dropdownOpen}
|
||||||
class="input input-bordered flex justify-between items-center cursor-pointer"
|
class="input input-bordered flex justify-between items-center cursor-pointer"
|
||||||
on:click={() => (dropdownOpen = !dropdownOpen)}
|
on:click={() => (dropdownOpen = !dropdownOpen)}
|
||||||
|
on:keydown={handleKeydown}
|
||||||
>
|
>
|
||||||
<span class="truncate">{selectedTimezone}</span>
|
<span class="truncate">{selectedTimezone}</span>
|
||||||
<svg
|
<svg
|
||||||
|
@ -49,6 +71,7 @@
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -58,6 +81,8 @@
|
||||||
{#if dropdownOpen}
|
{#if dropdownOpen}
|
||||||
<div
|
<div
|
||||||
class="absolute mt-1 z-10 bg-base-100 shadow-lg rounded-box w-full max-h-60 overflow-y-auto"
|
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 -->
|
<!-- Search -->
|
||||||
<div class="sticky top-0 bg-base-100 p-2 border-b">
|
<div class="sticky top-0 bg-base-100 p-2 border-b">
|
||||||
|
@ -66,7 +91,7 @@
|
||||||
placeholder="Search timezone"
|
placeholder="Search timezone"
|
||||||
class="input input-sm input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
autofocus
|
bind:this={searchInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -75,12 +100,16 @@
|
||||||
<ul class="menu p-2 space-y-1">
|
<ul class="menu p-2 space-y-1">
|
||||||
{#each filteredTimezones as tz}
|
{#each filteredTimezones as tz}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<button
|
||||||
class={`truncate ${tz === selectedTimezone ? 'active font-bold' : ''}`}
|
type="button"
|
||||||
on:click|preventDefault={() => selectTimezone(tz)}
|
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}
|
{tz}
|
||||||
</a>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -6,37 +6,23 @@
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
let modal: HTMLDialogElement;
|
let modal: HTMLDialogElement;
|
||||||
import { t } from 'svelte-i18n';
|
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 MarkdownEditor from './MarkdownEditor.svelte';
|
||||||
import { appVersion } from '$lib/config';
|
import { appVersion } from '$lib/config';
|
||||||
import { DefaultMarker, MapLibre } from 'svelte-maplibre';
|
import { DefaultMarker, MapLibre } from 'svelte-maplibre';
|
||||||
import TimezoneSelector from './TimezoneSelector.svelte';
|
import DateRangeCollapse from './DateRangeCollapse.svelte';
|
||||||
|
|
||||||
export let collection: Collection;
|
export let collection: Collection;
|
||||||
export let transportationToEdit: Transportation | null = null;
|
export let transportationToEdit: Transportation | null = null;
|
||||||
|
|
||||||
let constrainDates: boolean = false;
|
|
||||||
|
|
||||||
// Initialize transportation object
|
// Initialize transportation object
|
||||||
let transportation: Transportation = {
|
let transportation: Transportation = {
|
||||||
id: transportationToEdit?.id || '',
|
id: transportationToEdit?.id || '',
|
||||||
type: transportationToEdit?.type || '',
|
type: transportationToEdit?.type || '',
|
||||||
name: transportationToEdit?.name || '',
|
name: transportationToEdit?.name || '',
|
||||||
description: transportationToEdit?.description || '',
|
description: transportationToEdit?.description || '',
|
||||||
date: null,
|
date: transportationToEdit?.date || null,
|
||||||
end_date: null,
|
end_date: transportationToEdit?.end_date || null,
|
||||||
rating: transportationToEdit?.rating || 0,
|
rating: transportationToEdit?.rating || 0,
|
||||||
link: transportationToEdit?.link || '',
|
link: transportationToEdit?.link || '',
|
||||||
flight_number: transportationToEdit?.flight_number || '',
|
flight_number: transportationToEdit?.flight_number || '',
|
||||||
|
@ -53,58 +39,20 @@
|
||||||
destination_longitude: transportationToEdit?.destination_longitude || NaN
|
destination_longitude: transportationToEdit?.destination_longitude || NaN
|
||||||
};
|
};
|
||||||
|
|
||||||
let fullStartDate: string = '';
|
|
||||||
let fullEndDate: string = '';
|
|
||||||
|
|
||||||
let starting_airport: string = '';
|
let starting_airport: string = '';
|
||||||
let ending_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) {
|
if (!transportation.rating) {
|
||||||
transportation.rating = NaN;
|
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 () => {
|
onMount(async () => {
|
||||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.showModal();
|
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() {
|
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) {
|
async function geocode(e: Event | null) {
|
||||||
// Geocoding logic unchanged
|
// Geocoding logic unchanged
|
||||||
if (e) {
|
if (e) {
|
||||||
|
@ -214,30 +150,9 @@
|
||||||
Math.round(transportation.destination_longitude * 1e6) / 1e6;
|
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
|
// Use the stored UTC dates for submission
|
||||||
const submissionData = {
|
const submissionData = {
|
||||||
...transportation,
|
...transportation
|
||||||
date: utcStartDate,
|
|
||||||
end_date: utcEndDate
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (transportation.type != 'plane') {
|
if (transportation.type != 'plane') {
|
||||||
|
@ -255,18 +170,6 @@
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
transportation = data as Transportation;
|
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'));
|
addToast('success', $t('adventures.adventure_created'));
|
||||||
dispatch('save', transportation);
|
dispatch('save', transportation);
|
||||||
|
@ -285,18 +188,6 @@
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
transportation = data as Transportation;
|
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'));
|
addToast('success', $t('adventures.adventure_updated'));
|
||||||
dispatch('save', transportation);
|
dispatch('save', transportation);
|
||||||
|
@ -447,96 +338,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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
|
<DateRangeCollapse
|
||||||
class="label cursor-pointer flex items-start space-x-2"
|
type="transportation"
|
||||||
>
|
bind:utcStartDate={transportation.date}
|
||||||
<span class="label-text">{$t('adventures.date_constrain')}</span>
|
bind:utcEndDate={transportation.end_date}
|
||||||
<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>
|
|
||||||
<!-- Flight Information -->
|
<!-- Flight Information -->
|
||||||
|
|
||||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
|
|
@ -26,10 +26,22 @@ export function toLocalDatetime(
|
||||||
*/
|
*/
|
||||||
export function toUTCDatetime(
|
export function toUTCDatetime(
|
||||||
localDate: string,
|
localDate: string,
|
||||||
timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone
|
timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
allDay: boolean = false
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!localDate) return 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
|
* @param params Object containing local date and timezone
|
||||||
* @returns Object with updated UTC datetime string
|
* @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 {
|
return {
|
||||||
utcDate: toUTCDatetime(localDate, timezone)
|
utcDate: toUTCDatetime(localDate, timezone, allDay)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<script>
|
|
||||||
import DateRangeDropdown from '$lib/components/DateRangeDropdown.svelte';
|
|
||||||
|
|
||||||
let utcStartDate = '';
|
|
||||||
let utcEndDate = '';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DateRangeDropdown bind:utcStartDate bind:utcEndDate />
|
|
||||||
|
|
||||||
<p>{new Date(utcStartDate).toLocaleString()} - {new Date(utcEndDate).toLocaleString()}</p>
|
|
||||||
|
|
||||||
<p>UTC Start Date: {utcStartDate}</p>
|
|
||||||
<p>UTC End Date: {utcEndDate}</p>
|
|
Loading…
Add table
Add a link
Reference in a new issue