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

Enhance timezone handling in AdventureModal and DateRangeCollapse components; add support for departure and arrival timezones in the TimezoneSelector and update localization for new timezone labels.

This commit is contained in:
Sean Morley 2025-05-10 10:47:00 -04:00
parent 89c4f1058a
commit b30d6df964
4 changed files with 94 additions and 101 deletions

View file

@ -390,54 +390,6 @@
} }
} }
let new_start_date: string = '';
let new_end_date: string = '';
let new_notes: string = '';
// Function to add a new visit.
function addNewVisit() {
// If an end date isnt provided, assume its the same as start.
if (new_start_date && !new_end_date) {
new_end_date = new_start_date;
}
if (new_start_date > new_end_date) {
addToast('error', $t('adventures.start_before_end_error'));
return;
}
if (new_end_date && !new_start_date) {
addToast('error', $t('adventures.no_start_date'));
return;
}
// Convert input to UTC if not already.
if (new_start_date && !new_start_date.includes('Z')) {
new_start_date = new Date(new_start_date).toISOString();
}
if (new_end_date && !new_end_date.includes('Z')) {
new_end_date = new Date(new_end_date).toISOString();
}
// If the visit is all day, force the times to midnight.
if (allDay) {
new_start_date = new_start_date.split('T')[0] + 'T00:00:00.000Z';
new_end_date = new_end_date.split('T')[0] + 'T00:00:00.000Z';
}
adventure.visits = [
...adventure.visits,
{
start_date: new_start_date,
end_date: new_end_date,
notes: new_notes,
id: '' // or generate an id as needed
}
];
// Clear the input fields.
new_start_date = '';
new_end_date = '';
new_notes = '';
}
function close() { function close() {
dispatch('close'); dispatch('close');
} }

View file

@ -10,7 +10,8 @@
export let type: 'adventure' | 'transportation' | 'lodging' = 'adventure'; export let type: 'adventure' | 'transportation' | 'lodging' = 'adventure';
// Initialize with browser's timezone // Initialize with browser's timezone
let selectedTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; let selectedStartTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
let selectedEndTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
let allDay: boolean = false; let allDay: boolean = false;
@ -18,15 +19,14 @@
export let utcStartDate: string | null = null; export let utcStartDate: string | null = null;
export let utcEndDate: 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; export let note: string | null = null;
type Visit = { type Visit = {
id: string; id: string;
start_date: string; start_date: string;
end_date: string; end_date: string;
notes: string; notes: string;
start_timezone?: string;
end_timezone?: string;
}; };
export let visits: Visit[] | null = null; export let visits: Visit[] | null = null;
@ -42,17 +42,15 @@
let isEditing = false; // Disable reactivity when editing let isEditing = false; // Disable reactivity when editing
onMount(async () => { onMount(async () => {
console.log('Selected timezone:', selectedTimezone); // Initialize UTC dates
console.log('UTC Start Date:', utcStartDate);
console.log('UTC End Date:', utcEndDate);
// Initialize UTC dates from transportationToEdit if available
localStartDate = updateLocalDate({ localStartDate = updateLocalDate({
utcDate: utcStartDate, utcDate: utcStartDate,
timezone: selectedTimezone timezone: selectedStartTimezone
}).localDate; }).localDate;
localEndDate = updateLocalDate({ localEndDate = updateLocalDate({
utcDate: utcEndDate, utcDate: utcEndDate,
timezone: selectedTimezone timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
}).localDate; }).localDate;
}); });
@ -82,12 +80,12 @@
} else { } else {
const start = updateLocalDate({ const start = updateLocalDate({
utcDate: utcStartDate, utcDate: utcStartDate,
timezone: selectedTimezone timezone: selectedStartTimezone
}).localDate; }).localDate;
const end = updateLocalDate({ const end = updateLocalDate({
utcDate: utcEndDate, utcDate: utcEndDate,
timezone: selectedTimezone timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
}).localDate; }).localDate;
localStartDate = start; localStartDate = start;
@ -99,16 +97,34 @@
function handleLocalDateChange() { function handleLocalDateChange() {
utcStartDate = updateUTCDate({ utcStartDate = updateUTCDate({
localDate: localStartDate, localDate: localStartDate,
timezone: selectedTimezone, timezone: selectedStartTimezone,
allDay allDay
}).utcDate; }).utcDate;
utcEndDate = updateUTCDate({ utcEndDate = updateUTCDate({
localDate: localEndDate, localDate: localEndDate,
timezone: selectedTimezone, timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone,
allDay allDay
}).utcDate; }).utcDate;
} }
// Create a visit object with appropriate timezone information
function createVisitObject() {
const newVisit: Visit = {
id: crypto.randomUUID(),
start_date: utcStartDate ?? '',
end_date: utcEndDate ?? utcStartDate ?? '',
notes: note ?? ''
};
// For transportation, add timezone information
if (type === 'transportation') {
newVisit.start_timezone = selectedStartTimezone;
newVisit.end_timezone = selectedEndTimezone;
}
return newVisit;
}
</script> </script>
<div class="collapse collapse-plus bg-base-200 mb-4 rounded-lg"> <div class="collapse collapse-plus bg-base-200 mb-4 rounded-lg">
@ -117,14 +133,32 @@
{$t('adventures.date_information')} {$t('adventures.date_information')}
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<!-- Timezone Selector --> <!-- Timezone Selector Section -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"></div>
<div class="rounded-xl border border-base-300 bg-base-100 p-4 space-y-4 shadow-sm mb-4"> <div class="rounded-xl border border-base-300 bg-base-100 p-4 space-y-4 shadow-sm mb-4">
<!-- Group Header --> <!-- Group Header -->
<h3 class="text-md font-semibold">{$t('navbar.settings')}</h3> <h3 class="text-md font-semibold">{$t('navbar.settings')}</h3>
<TimezoneSelector bind:selectedTimezone /> {#if type === 'transportation'}
<!-- Dual timezone selectors for transportation -->
<div class="space-y-4">
<div>
<label class="text-sm font-medium block mb-1">
{$t('adventures.departure_timezone')}
</label>
<TimezoneSelector bind:selectedTimezone={selectedStartTimezone} />
</div>
<div>
<label class="text-sm font-medium block mb-1">
{$t('adventures.arrival_timezone')}
</label>
<TimezoneSelector bind:selectedTimezone={selectedEndTimezone} />
</div>
</div>
{:else}
<!-- Single timezone selector for other types -->
<TimezoneSelector bind:selectedTimezone={selectedStartTimezone} />
{/if}
<!-- All Day Toggle --> <!-- All Day Toggle -->
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
@ -145,21 +179,21 @@
} }
utcStartDate = updateUTCDate({ utcStartDate = updateUTCDate({
localDate: localStartDate, localDate: localStartDate,
timezone: selectedTimezone, timezone: selectedStartTimezone,
allDay allDay
}).utcDate; }).utcDate;
utcEndDate = updateUTCDate({ utcEndDate = updateUTCDate({
localDate: localEndDate, localDate: localEndDate,
timezone: selectedTimezone, timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone,
allDay allDay
}).utcDate; }).utcDate;
localStartDate = updateLocalDate({ localStartDate = updateLocalDate({
utcDate: utcStartDate, utcDate: utcStartDate,
timezone: selectedTimezone timezone: selectedStartTimezone
}).localDate; }).localDate;
localEndDate = updateLocalDate({ localEndDate = updateLocalDate({
utcDate: utcEndDate, utcDate: utcEndDate,
timezone: selectedTimezone timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
}).localDate; }).localDate;
}} }}
/> />
@ -185,7 +219,9 @@
<!-- Start Date --> <!-- Start Date -->
<div class="space-y-2"> <div class="space-y-2">
<label for="date" class="text-sm font-medium"> <label for="date" class="text-sm font-medium">
{$t('adventures.start_date')} {type === 'transportation'
? $t('adventures.departure_date')
: $t('adventures.start_date')}
</label> </label>
{#if allDay} {#if allDay}
@ -217,7 +253,7 @@
{#if localStartDate} {#if localStartDate}
<div class="space-y-2"> <div class="space-y-2">
<label for="end_date" class="text-sm font-medium"> <label for="end_date" class="text-sm font-medium">
{$t('adventures.end_date')} {type === 'transportation' ? $t('adventures.arrival_date') : $t('adventures.end_date')}
</label> </label>
{#if allDay} {#if allDay}
@ -267,12 +303,7 @@
class="btn btn-primary mb-2" class="btn btn-primary mb-2"
type="button" type="button"
on:click={() => { on:click={() => {
const newVisit = { const newVisit = createVisitObject();
id: crypto.randomUUID(),
start_date: utcStartDate ?? '',
end_date: utcEndDate ?? utcStartDate ?? '',
notes: note ?? ''
};
// Ensure reactivity by assigning a *new* array // Ensure reactivity by assigning a *new* array
if (visits) { if (visits) {
@ -345,7 +376,12 @@
{/if} {/if}
</p> </p>
<!-- If the selected timezone is not the current one show the timezone + the time converted there --> <!-- Display timezone information for transportation visits -->
{#if visit.start_timezone && visit.end_timezone && visit.start_timezone !== visit.end_timezone}
<p class="text-xs text-base-content">
{visit.start_timezone}{visit.end_timezone}
</p>
{/if}
{#if visit.notes} {#if visit.notes}
<p class="text-sm text-base-content opacity-70 italic"> <p class="text-sm text-base-content opacity-70 italic">
@ -362,24 +398,24 @@
const isAllDayEvent = isAllDay(visit.start_date); const isAllDayEvent = isAllDay(visit.start_date);
allDay = isAllDayEvent; allDay = isAllDayEvent;
// Set timezone information if available
if (visit.start_timezone) selectedStartTimezone = visit.start_timezone;
if (visit.end_timezone) selectedEndTimezone = visit.end_timezone;
if (isAllDayEvent) { if (isAllDayEvent) {
localStartDate = visit.start_date.split('T')[0]; localStartDate = visit.start_date.split('T')[0];
localEndDate = visit.end_date.split('T')[0]; localEndDate = visit.end_date.split('T')[0];
} else { } else {
const startDate = new Date(visit.start_date); // Update with timezone awareness
const endDate = new Date(visit.end_date); localStartDate = updateLocalDate({
utcDate: visit.start_date,
timezone: selectedStartTimezone
}).localDate;
localStartDate = `${startDate.getFullYear()}-${String( localEndDate = updateLocalDate({
startDate.getMonth() + 1 utcDate: visit.end_date,
).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}T${String( timezone: visit.end_timezone || selectedStartTimezone
startDate.getHours() }).localDate;
).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 // remove it from visits
@ -391,7 +427,6 @@
constrainDates = true; constrainDates = true;
utcStartDate = visit.start_date; utcStartDate = visit.start_date;
utcEndDate = visit.end_date; utcEndDate = visit.end_date;
type = 'adventure';
setTimeout(() => { setTimeout(() => {
isEditing = false; isEditing = false;

View file

@ -3,10 +3,12 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let selectedTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; export let selectedTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Generate a unique ID for this component instance
const instanceId = `tz-selector-${crypto.randomUUID().substring(0, 8)}`;
let dropdownOpen = false; let dropdownOpen = false;
let searchQuery = ''; let searchQuery = '';
let searchInput: HTMLInputElement; let searchInput: HTMLInputElement | null = null;
const timezones = Intl.supportedValuesOf('timeZone'); const timezones = Intl.supportedValuesOf('timeZone');
// Filter timezones based on search query // Filter timezones based on search query
@ -20,10 +22,12 @@
searchQuery = ''; searchQuery = '';
} }
// Focus search input when dropdown opens // Focus search input when dropdown opens - with proper null check
$: if (dropdownOpen && searchInput) { $: if (dropdownOpen && searchInput) {
// Use setTimeout to delay focus until after the element is rendered // Use setTimeout to delay focus until after the element is rendered
setTimeout(() => searchInput.focus(), 0); setTimeout(() => {
if (searchInput) searchInput.focus();
}, 0);
} }
function handleKeydown(event: KeyboardEvent, tz?: string) { function handleKeydown(event: KeyboardEvent, tz?: string) {
@ -40,7 +44,7 @@
// Close dropdown if clicked outside // Close dropdown if clicked outside
onMount(() => { onMount(() => {
const handleClickOutside = (e: MouseEvent) => { const handleClickOutside = (e: MouseEvent) => {
const dropdown = document.getElementById('tz-selector'); const dropdown = document.getElementById(instanceId);
if (dropdown && !dropdown.contains(e.target as Node)) dropdownOpen = false; if (dropdown && !dropdown.contains(e.target as Node)) dropdownOpen = false;
}; };
document.addEventListener('click', handleClickOutside); document.addEventListener('click', handleClickOutside);
@ -48,14 +52,14 @@
}); });
</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={instanceId}>
<label class="label" for="timezone-display"> <label class="label" for={`timezone-display-${instanceId}`}>
<span class="label-text">{$t('adventures.timezone')}</span> <span class="label-text">{$t('adventures.timezone')}</span>
</label> </label>
<!-- Trigger --> <!-- Trigger -->
<div <div
id="timezone-display" id={`timezone-display-${instanceId}`}
tabindex="0" tabindex="0"
role="button" role="button"
aria-haspopup="listbox" aria-haspopup="listbox"
@ -82,7 +86,7 @@
<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" role="listbox"
aria-labelledby="timezone-display" aria-labelledby={`timezone-display-${instanceId}`}
> >
<!-- 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">

View file

@ -65,6 +65,8 @@
"invalid_date_range": "Invalid date range", "invalid_date_range": "Invalid date range",
"timezone": "Timezone", "timezone": "Timezone",
"no_visits": "No visits", "no_visits": "No visits",
"departure_timezone": "Departure Timezone",
"arrival_timezone": "Arrival Timezone",
"no_image_found": "No image found", "no_image_found": "No image found",
"collection_link_error": "Error linking adventure to collection", "collection_link_error": "Error linking adventure to collection",
"adventure_delete_confirm": "Are you sure you want to delete this adventure? This action cannot be undone.", "adventure_delete_confirm": "Are you sure you want to delete this adventure? This action cannot be undone.",