From eef8c92e8215f9efd3d84932598bc7c3213e3a23 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 18 Jun 2025 19:03:32 -0400 Subject: [PATCH 01/10] feat(calendar): enhance event handling with timezone support and filtering capabilities --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 11 + .../src/lib/components/LodgingCard.svelte | 19 +- .../src/lib/components/LodgingModal.svelte | 17 +- frontend/src/lib/dateUtils.ts | 19 + frontend/src/locales/de.json | 14 + frontend/src/locales/en.json | 14 + frontend/src/locales/es.json | 14 + frontend/src/locales/fr.json | 14 + frontend/src/locales/it.json | 14 + frontend/src/locales/ko.json | 14 + frontend/src/locales/nl.json | 14 + frontend/src/locales/no.json | 14 + frontend/src/locales/pl.json | 14 + frontend/src/locales/ru.json | 14 + frontend/src/locales/sv.json | 14 + frontend/src/locales/zh.json | 14 + frontend/src/routes/calendar/+page.server.ts | 100 ++++- frontend/src/routes/calendar/+page.svelte | 411 +++++++++++++++++- .../src/routes/collections/[id]/+page.svelte | 117 +++-- 20 files changed, 793 insertions(+), 70 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index b0a09fb..95ff6c9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@event-calendar/core": "^3.7.1", "@event-calendar/day-grid": "^3.7.1", + "@event-calendar/interaction": "^3.12.0", "@event-calendar/time-grid": "^3.7.1", "@iconify-json/mdi": "^1.1.67", "@sveltejs/adapter-node": "^5.2.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3faf76d..aa4490a 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: '@event-calendar/day-grid': specifier: ^3.7.1 version: 3.12.0 + '@event-calendar/interaction': + specifier: ^3.12.0 + version: 3.12.0 '@event-calendar/time-grid': specifier: ^3.7.1 version: 3.12.0 @@ -566,6 +569,9 @@ packages: '@event-calendar/day-grid@3.12.0': resolution: {integrity: sha512-gY6XvEIlwWI9uKWsXukyanDmrEWv1UDHdhikhchpe6iZP25p3+760qXIU2kdu91tXjb+hVbpFcn7sdNPPE4u7Q==} + '@event-calendar/interaction@3.12.0': + resolution: {integrity: sha512-+d3KqxNdcY/RfJrdai37XCoTx7KKpzqJIo/WAjH1p8ZiypsfrHgpWWuTtF76u3hpn/1qqWUM3VFJSTKbjJkWTg==} + '@event-calendar/time-grid@3.12.0': resolution: {integrity: sha512-n/IoFSq/ym6ad2k+H9RL2A8GpfOJy1zpKKLb1Edp/QEusexpPg8LNdSbxhmKGz6ip5ud0Bi/xgUa8xUqut8ooQ==} @@ -2405,6 +2411,11 @@ snapshots: '@event-calendar/core': 3.12.0 svelte: 4.2.19 + '@event-calendar/interaction@3.12.0': + dependencies: + '@event-calendar/core': 3.12.0 + svelte: 4.2.19 + '@event-calendar/time-grid@3.12.0': dependencies: '@event-calendar/core': 3.12.0 diff --git a/frontend/src/lib/components/LodgingCard.svelte b/frontend/src/lib/components/LodgingCard.svelte index 148e42a..fe27bff 100644 --- a/frontend/src/lib/components/LodgingCard.svelte +++ b/frontend/src/lib/components/LodgingCard.svelte @@ -8,6 +8,8 @@ import DeleteWarning from './DeleteWarning.svelte'; import { LODGING_TYPES_ICONS } from '$lib'; import { formatDateInTimezone } from '$lib/dateUtils'; + import { formatAllDayDate } from '$lib/dateUtils'; + import { isAllDay } from '$lib'; const dispatch = createEventDispatcher(); @@ -96,8 +98,8 @@ >
-
-

{lodging.name}

+
+

{lodging.name}

{$t(`lodging.${lodging.type}`)} @@ -122,10 +124,15 @@
{$t('adventures.dates')}:

- {formatDateInTimezone(lodging.check_in, lodging.timezone)} – - {formatDateInTimezone(lodging.check_out, lodging.timezone)} - {#if lodging.timezone} - ({lodging.timezone}) + {#if isAllDay(lodging.check_in)} + {formatAllDayDate(lodging.check_in)} – + {formatAllDayDate(lodging.check_out)} + {:else} + {formatDateInTimezone(lodging.check_in, lodging.timezone)} – + {formatDateInTimezone(lodging.check_out, lodging.timezone)} + {#if lodging.timezone} + ({lodging.timezone}) + {/if} {/if}

diff --git a/frontend/src/lib/components/LodgingModal.svelte b/frontend/src/lib/components/LodgingModal.svelte index 239211b..594d505 100644 --- a/frontend/src/lib/components/LodgingModal.svelte +++ b/frontend/src/lib/components/LodgingModal.svelte @@ -22,19 +22,7 @@ label: string; }; - const LODGING_TYPES: LodgingType[] = [ - { value: 'hotel', label: 'Hotel' }, - { value: 'hostel', label: 'Hostel' }, - { value: 'resort', label: 'Resort' }, - { value: 'bnb', label: 'Bed & Breakfast' }, - { value: 'campground', label: 'Campground' }, - { value: 'cabin', label: 'Cabin' }, - { value: 'apartment', label: 'Apartment' }, - { value: 'house', label: 'House' }, - { value: 'villa', label: 'Villa' }, - { value: 'motel', label: 'Motel' }, - { value: 'other', label: 'Other' } - ]; + let lodgingTimezone: string | undefined = lodging.timezone ?? undefined; // Initialize hotel with values from lodgingToEdit or default values function initializeLodging(lodgingToEdit: Lodging | null): Lodging { @@ -304,7 +292,8 @@ type="lodging" bind:utcStartDate={lodging.check_in} bind:utcEndDate={lodging.check_out} - bind:selectedStartTimezone={lodging.timezone} + bind:selectedStartTimezone={lodgingTimezone} + {collection} /> diff --git a/frontend/src/lib/dateUtils.ts b/frontend/src/lib/dateUtils.ts index 38bba60..a844f76 100644 --- a/frontend/src/lib/dateUtils.ts +++ b/frontend/src/lib/dateUtils.ts @@ -147,6 +147,25 @@ export function formatUTCDate(utcDate: string | null): string { return dateTime.toISO()?.slice(0, 16).replace('T', ' ') || ''; } +/** + * Format all-day date for display without timezone conversion + * @param dateString - Date string in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + * @returns Formatted date string (e.g., "Jun 1, 2025") + */ +export function formatAllDayDate(dateString: string): string { + if (!dateString) return ''; + + // Extract just the date part and add midday time to avoid timezone issues + const datePart = dateString.split('T')[0]; + const dateWithMidday = `${datePart}T12:00:00`; + + return new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }).format(new Date(dateWithMidday)); +} + export const VALID_TIMEZONES = [ 'Africa/Abidjan', 'Africa/Accra', diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 2194e1d..33308ba 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -691,5 +691,19 @@ }, "google_maps": { "google_maps_integration_desc": "Verbinden Sie Ihr Google Maps-Konto, um hochwertige Suchergebnisse und Empfehlungen für Standort zu erhalten." + }, + "calendar": { + "all_categories": "Alle Kategorien", + "all_day_event": "Ganztägige Veranstaltung", + "calendar_overview": "Kalenderübersicht", + "categories": "Kategorien", + "day": "Tag", + "events_scheduled": "Veranstaltungen geplant", + "filter_by_category": "Filter nach Kategorie", + "filtered_results": "Gefilterte Ergebnisse", + "month": "Monat", + "today": "Heute", + "total_events": "Gesamtereignisse", + "week": "Woche" } } diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 8f19349..82e2fa4 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -691,5 +691,19 @@ "adventure_recommendations": "Adventure Recommendations", "food": "Food", "tourism": "Tourism" + }, + "calendar": { + "today": "Today", + "month": "Month", + "week": "Week", + "day": "Day", + "events_scheduled": "events scheduled", + "total_events": "Total Events", + "all_categories": "All Categories", + "calendar_overview": "Calendar Overview", + "categories": "Categories", + "filtered_results": "Filtered Results", + "filter_by_category": "Filter by Category", + "all_day_event": "All Day Event" } } diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 4ab423f..61f54e7 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -691,5 +691,19 @@ }, "google_maps": { "google_maps_integration_desc": "Conecte su cuenta de Google Maps para obtener resultados y recomendaciones de búsqueda de ubicación de alta calidad." + }, + "calendar": { + "all_categories": "Todas las categorías", + "all_day_event": "Evento todo el día", + "calendar_overview": "Descripción general del calendario", + "categories": "Categorías", + "day": "Día", + "events_scheduled": "Eventos programados", + "filter_by_category": "Filtrar por categoría", + "filtered_results": "Resultados filtrados", + "month": "Mes", + "today": "Hoy", + "total_events": "Total de eventos", + "week": "Semana" } } diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index 377f396..c8412ce 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -691,5 +691,19 @@ }, "google_maps": { "google_maps_integration_desc": "Connectez votre compte Google Maps pour obtenir des résultats de recherche et recommandations de recherche de haute qualité." + }, + "calendar": { + "all_categories": "Toutes les catégories", + "all_day_event": "Événement toute la journée", + "calendar_overview": "Aperçu du calendrier", + "categories": "Catégories", + "day": "Jour", + "events_scheduled": "événements prévus", + "filter_by_category": "Filtre par catégorie", + "filtered_results": "Résultats filtrés", + "month": "Mois", + "today": "Aujourd'hui", + "total_events": "Événements totaux", + "week": "Semaine" } } diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 71ed2ee..9a1060b 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -691,5 +691,19 @@ }, "google_maps": { "google_maps_integration_desc": "Collega il tuo account Google Maps per ottenere risultati e consigli di ricerca sulla posizione di alta qualità." + }, + "calendar": { + "all_categories": "Tutte le categorie", + "all_day_event": "Evento per tutto il giorno", + "calendar_overview": "Panoramica del calendario", + "categories": "Categorie", + "day": "Giorno", + "events_scheduled": "eventi programmati", + "filter_by_category": "Filtro per categoria", + "filtered_results": "Risultati filtrati", + "month": "Mese", + "today": "Oggi", + "total_events": "Eventi totali", + "week": "Settimana" } } diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json index 85aad1c..3b2cd2d 100644 --- a/frontend/src/locales/ko.json +++ b/frontend/src/locales/ko.json @@ -690,5 +690,19 @@ }, "google_maps": { "google_maps_integration_desc": "Google지도 계정을 연결하여 고품질 위치 검색 결과 및 권장 사항을 얻으십시오." + }, + "calendar": { + "all_categories": "모든 카테고리", + "all_day_event": "하루 종일 이벤트", + "calendar_overview": "캘린더 개요", + "categories": "카테고리", + "day": "낮", + "events_scheduled": "예약 된 이벤트", + "filter_by_category": "카테고리 별 필터", + "filtered_results": "필터링 된 결과", + "month": "월", + "today": "오늘", + "total_events": "총 이벤트", + "week": "주" } } diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index 69eb2fe..e060694 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -691,5 +691,19 @@ }, "google_maps": { "google_maps_integration_desc": "Sluit uw Google Maps-account aan om zoekresultaten en aanbevelingen van hoge kwaliteit te krijgen." + }, + "calendar": { + "all_categories": "Alle categorieën", + "all_day_event": "De hele dag evenement", + "calendar_overview": "Kalenderoverzicht", + "categories": "Categorieën", + "day": "Dag", + "events_scheduled": "geplande evenementen", + "filter_by_category": "Filter per categorie", + "filtered_results": "Gefilterde resultaten", + "month": "Maand", + "today": "Vandaag", + "total_events": "Totale gebeurtenissen", + "week": "Week" } } diff --git a/frontend/src/locales/no.json b/frontend/src/locales/no.json index 5e2583f..b703275 100644 --- a/frontend/src/locales/no.json +++ b/frontend/src/locales/no.json @@ -691,5 +691,19 @@ }, "google_maps": { "google_maps_integration_desc": "Koble til Google Maps-kontoen din for å få søkeresultater og anbefalinger av høy kvalitet." + }, + "calendar": { + "all_categories": "Alle kategorier", + "all_day_event": "Hele dagens arrangement", + "calendar_overview": "Kalenderoversikt", + "categories": "Kategorier", + "day": "Dag", + "events_scheduled": "hendelser planlagt", + "filter_by_category": "Filter etter kategori", + "filtered_results": "Filtrerte resultater", + "month": "Måned", + "today": "I dag", + "total_events": "Total hendelser", + "week": "Uke" } } diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index 99c79cf..51c0324 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -691,5 +691,19 @@ }, "google_maps": { "google_maps_integration_desc": "Połącz swoje konto Google Maps, aby uzyskać wysokiej jakości wyniki wyszukiwania i zalecenia dotyczące lokalizacji." + }, + "calendar": { + "all_categories": "Wszystkie kategorie", + "all_day_event": "Wydarzenie przez cały dzień", + "calendar_overview": "Przegląd kalendarza", + "categories": "Kategorie", + "day": "Dzień", + "events_scheduled": "Zaplanowane wydarzenia", + "filter_by_category": "Filtr według kategorii", + "filtered_results": "Przefiltrowane wyniki", + "month": "Miesiąc", + "today": "Dzisiaj", + "total_events": "Całkowite zdarzenia", + "week": "Tydzień" } } diff --git a/frontend/src/locales/ru.json b/frontend/src/locales/ru.json index cb3ad40..149d2f1 100644 --- a/frontend/src/locales/ru.json +++ b/frontend/src/locales/ru.json @@ -691,5 +691,19 @@ "adventure_recommendations": "Рекомендации приключений", "food": "Еда", "tourism": "Туризм" + }, + "calendar": { + "all_categories": "Все категории", + "all_day_event": "Событие на весь день", + "calendar_overview": "Обзор календаря", + "categories": "Категории", + "day": "День", + "events_scheduled": "События запланированы", + "filter_by_category": "Фильтр по категории", + "filtered_results": "Отфильтрованные результаты", + "month": "Месяц", + "today": "Сегодня", + "total_events": "Общее количество событий", + "week": "Неделя" } } diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 249c2c3..309b984 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -691,5 +691,19 @@ }, "google_maps": { "google_maps_integration_desc": "Anslut ditt Google Maps-konto för att få sökresultat och rekommendationer av hög kvalitet." + }, + "calendar": { + "all_categories": "Alla kategorier", + "all_day_event": "Hela dagen", + "calendar_overview": "Kalenderöversikt", + "categories": "Kategorier", + "day": "Dag", + "events_scheduled": "Händelser planerade", + "filter_by_category": "Filter efter kategori", + "filtered_results": "Filtrerade resultat", + "month": "Månad", + "today": "I dag", + "total_events": "Totala evenemang", + "week": "Vecka" } } diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 2233af4..f111b07 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -691,5 +691,19 @@ }, "google_maps": { "google_maps_integration_desc": "连接您的Google Maps帐户以获取高质量的位置搜索结果和建议。" + }, + "calendar": { + "all_categories": "所有类别", + "all_day_event": "全天活动", + "calendar_overview": "日历概述", + "categories": "类别", + "day": "天", + "events_scheduled": "预定事件", + "filter_by_category": "按类别过滤", + "filtered_results": "过滤结果", + "month": "月", + "today": "今天", + "total_events": "总事件", + "week": "星期" } } diff --git a/frontend/src/routes/calendar/+page.server.ts b/frontend/src/routes/calendar/+page.server.ts index 93ce8ed..0c46d7d 100644 --- a/frontend/src/routes/calendar/+page.server.ts +++ b/frontend/src/routes/calendar/+page.server.ts @@ -1,5 +1,8 @@ import type { Adventure } from '$lib/types'; import type { PageServerLoad } from './$types'; +import { formatDateInTimezone, formatAllDayDate } from '$lib/dateUtils'; +import { isAllDay } from '$lib'; + const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -12,21 +15,112 @@ export const load = (async (event) => { }); let adventures = (await visitedFetch.json()) as Adventure[]; + // Get user's local timezone as fallback + const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + let dates: Array<{ id: string; start: string; end: string; title: string; backgroundColor?: string; + extendedProps?: { + adventureName: string; + category: string; + icon: string; + timezone: string; + isAllDay: boolean; + formattedStart: string; + formattedEnd: string; + location?: string; + description?: string; + }; }> = []; + adventures.forEach((adventure) => { adventure.visits.forEach((visit) => { if (visit.start_date) { + let startDate = visit.start_date; + let endDate = visit.end_date || visit.start_date; + const targetTimezone = visit.timezone || userTimezone; + const allDay = isAllDay(visit.start_date); + + // Handle timezone conversion for non-all-day events + if (!allDay) { + // Convert UTC dates to target timezone + const startDateTime = new Date(visit.start_date); + const endDateTime = new Date(visit.end_date || visit.start_date); + + // Format for calendar (ISO string in target timezone) + startDate = new Intl.DateTimeFormat('sv-SE', { + timeZone: targetTimezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hourCycle: 'h23' + }) + .format(startDateTime) + .replace(' ', 'T'); + + endDate = new Intl.DateTimeFormat('sv-SE', { + timeZone: targetTimezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hourCycle: 'h23' + }) + .format(endDateTime) + .replace(' ', 'T'); + } else { + // For all-day events, use just the date part + startDate = visit.start_date.split('T')[0]; + + // For all-day events, add one day to end date to make it inclusive + const endDateObj = new Date(visit.end_date || visit.start_date); + endDateObj.setDate(endDateObj.getDate() + 1); + endDate = endDateObj.toISOString().split('T')[0]; + } + + // Create detailed title with timezone info + let detailedTitle = adventure.name; + if (adventure.category?.icon) { + detailedTitle = `${adventure.category.icon} ${detailedTitle}`; + } + + // Add time info to title for non-all-day events + if (!allDay) { + const startTime = formatDateInTimezone(visit.start_date, targetTimezone); + detailedTitle += ` (${startTime.split(' ').slice(-2).join(' ')})`; + if (targetTimezone !== userTimezone) { + detailedTitle += ` ${targetTimezone}`; + } + } + dates.push({ id: adventure.id, - start: visit.start_date, - end: visit.end_date || visit.start_date, - title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '') + start: startDate, + end: endDate, + title: detailedTitle, + backgroundColor: '#3b82f6', + extendedProps: { + adventureName: adventure.name, + category: adventure.category?.name || 'Adventure', + icon: adventure.category?.icon || '🗺️', + timezone: targetTimezone, + isAllDay: allDay, + formattedStart: allDay + ? formatAllDayDate(visit.start_date) + : formatDateInTimezone(visit.start_date, targetTimezone), + formattedEnd: allDay + ? formatAllDayDate(visit.end_date || visit.start_date) + : formatDateInTimezone(visit.end_date || visit.start_date, targetTimezone), + location: adventure.location || '', + description: adventure.description || '' + } }); } }); diff --git a/frontend/src/routes/calendar/+page.svelte b/frontend/src/routes/calendar/+page.svelte index 99f233a..148f94a 100644 --- a/frontend/src/routes/calendar/+page.svelte +++ b/frontend/src/routes/calendar/+page.svelte @@ -1,38 +1,421 @@ -

{$t('adventures.adventure_calendar')}

+ + {$t('adventures.adventure_calendar')} - AdventureLog + - +
+
+ - -
- {$t('adventures.download_calendar')} +
+ +
+
+
+
+ +
+
+ +
+
+

+ {$t('adventures.adventure_calendar')} +

+

+ {filteredDates.length} + {$t('calendar.events_scheduled')} +

+
+
+
+ + + +
+ + +
+
+ + + {#if searchFilter.length > 0} + + {/if} +
+
+ + +
+ {$t('worldtravel.filter_by')}: + {#if searchFilter} + + {/if} +
+
+
+ + +
+ +
+
+ +
+
+
+
+ + +
+ +
+
+ +
+
+ +
+

{$t('adventures.filters_and_stats')}

+
+ + +
+

+ + {$t('calendar.calendar_overview')} +

+ +
+
+
{$t('calendar.total_events')}
+
{allDates.length}
+
+ +
+
+
{$t('navbar.adventures')}
+
{adventures.length}
+
+
+ + {#if filteredDates.length !== allDates.length} +
+
+ {$t('calendar.filtered_results')} + {filteredDates.length} {$t('worldtravel.of')} {allDates.length} +
+ +
+ {/if} +
+
+ + +
+ + + {$t('adventures.download_calendar')} + + + +
+
+
+
+
+ + +{#if showEventModal && selectedEvent} + + +{/if} diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 5f64513..474e9fd 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -17,12 +17,13 @@ import Plus from '~icons/mdi/plus'; import AdventureCard from '$lib/components/AdventureCard.svelte'; import AdventureLink from '$lib/components/AdventureLink.svelte'; - import NotFound from '$lib/components/NotFound.svelte'; - import { DefaultMarker, MapLibre, Marker, Popup, LineLayer, GeoJSON } from 'svelte-maplibre'; + import { MapLibre, Marker, Popup, LineLayer, GeoJSON } from 'svelte-maplibre'; import TransportationCard from '$lib/components/TransportationCard.svelte'; import NoteCard from '$lib/components/NoteCard.svelte'; import NoteModal from '$lib/components/NoteModal.svelte'; + const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + import { groupAdventuresByDate, groupNotesByDate, @@ -31,8 +32,11 @@ osmTagToEmoji, groupLodgingByDate, LODGING_TYPES_ICONS, - getBasemapUrl + getBasemapUrl, + isAllDay } from '$lib'; + import { formatDateInTimezone, formatAllDayDate } from '$lib/dateUtils'; + import ChecklistCard from '$lib/components/ChecklistCard.svelte'; import ChecklistModal from '$lib/components/ChecklistModal.svelte'; import AdventureModal from '$lib/components/AdventureModal.svelte'; @@ -60,19 +64,6 @@ let collection: Collection; - // add christmas and new years - // dates = Array.from({ length: 25 }, (_, i) => { - // const date = new Date(); - // date.setMonth(11); - // date.setDate(i + 1); - // return { - // id: i.toString(), - // start: date.toISOString(), - // end: date.toISOString(), - // title: '🎄' - // }; - // }); - let dates: Array<{ id: string; start: string; @@ -93,16 +84,79 @@ dates = []; if (adventures) { - dates = dates.concat( - adventures.flatMap((adventure) => - adventure.visits.map((visit) => ({ - id: adventure.id, - start: visit.start_date || '', // Ensure it's a string - end: visit.end_date || visit.start_date || '', // Ensure it's a string - title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '') - })) - ) - ); + adventures.forEach((adventure) => { + adventure.visits.forEach((visit) => { + if (visit.start_date) { + let startDate = visit.start_date; + let endDate = visit.end_date || visit.start_date; + const targetTimezone = visit.timezone || userTimezone; + const allDay = isAllDay(visit.start_date); + + // Handle timezone conversion for non-all-day events + if (!allDay) { + // Convert UTC dates to target timezone + const startDateTime = new Date(visit.start_date); + const endDateTime = new Date(visit.end_date || visit.start_date); + + // Format for calendar (ISO string in target timezone) + startDate = new Intl.DateTimeFormat('sv-SE', { + timeZone: targetTimezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hourCycle: 'h23' + }) + .format(startDateTime) + .replace(' ', 'T'); + + endDate = new Intl.DateTimeFormat('sv-SE', { + timeZone: targetTimezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hourCycle: 'h23' + }) + .format(endDateTime) + .replace(' ', 'T'); + } else { + // For all-day events, use just the date part + startDate = visit.start_date.split('T')[0]; + + // For all-day events, add one day to end date to make it inclusive + const endDateObj = new Date(visit.end_date || visit.start_date); + endDateObj.setDate(endDateObj.getDate() + 1); + endDate = endDateObj.toISOString().split('T')[0]; + } + + // Create detailed title with timezone info + let detailedTitle = adventure.name; + if (adventure.category?.icon) { + detailedTitle = `${adventure.category.icon} ${detailedTitle}`; + } + + // Add time info to title for non-all-day events + if (!allDay) { + const startTime = formatDateInTimezone(visit.start_date, targetTimezone); + detailedTitle += ` (${startTime.split(' ').slice(-2).join(' ')})`; + if (targetTimezone !== userTimezone) { + detailedTitle += ` ${targetTimezone}`; + } + } + + dates.push({ + id: adventure.id, + start: startDate, + end: endDate, + title: detailedTitle, + backgroundColor: '#3b82f6' + }); + } + }); + }); } if (transportations) { @@ -113,7 +167,8 @@ id: transportation.id, start: transportation.date || '', // Ensure it's a string end: transportation.end_date || transportation.date || '', // Ensure it's a string - title: transportation.name + (transportation.type ? ` (${transportation.type})` : '') + title: transportation.name + (transportation.type ? ` (${transportation.type})` : ''), + backgroundColor: '#10b981' })) ); } @@ -126,7 +181,8 @@ id: lodging.id, start: lodging.check_in || '', // Ensure it's a string end: lodging.check_out || lodging.check_in || '', // Ensure it's a string - title: lodging.name + title: lodging.name, + backgroundColor: '#f59e0b' })) ); } @@ -140,11 +196,6 @@ let adventures: Adventure[] = []; - // Add this after your existing MapLibre markers - - // Add this after your existing MapLibre markers - - // Create line data from orderedItems $: lineData = createLineData(orderedItems); // Function to create GeoJSON line data from ordered items From 08cd3912c753e85f4d37c7e5bc232c63e7653319 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 18 Jun 2025 19:24:51 -0400 Subject: [PATCH 02/10] fix(config): correct appVersion string formatting --- frontend/src/lib/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index 4c07e0b..a9893c2 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -1,4 +1,4 @@ -export let appVersion = 'v0.10.0-main-06152025'; +export let appVersion = 'v0.10.0-main-06182025'; export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.10.0'; export let appTitle = 'AdventureLog'; export let copyrightYear = '2023-2025'; From 63e8e96d529ec0dedc0abcce596db3b45f87599c Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 18 Jun 2025 19:57:23 -0400 Subject: [PATCH 03/10] feat(collections): enhance lodging date handling with timezone support and all-day event formatting --- .../src/routes/collections/[id]/+page.svelte | 64 ++++++++++++++++--- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 474e9fd..cca5d6b 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -177,13 +177,61 @@ dates = dates.concat( lodging .filter((i) => i.check_in) - .map((lodging) => ({ - id: lodging.id, - start: lodging.check_in || '', // Ensure it's a string - end: lodging.check_out || lodging.check_in || '', // Ensure it's a string - title: lodging.name, - backgroundColor: '#f59e0b' - })) + .map((lodging) => { + const checkIn = lodging.check_in; + const checkOut = lodging.check_out || lodging.check_in; + if (!checkIn) return null; + + const isAlldayLodging: boolean = isAllDay(checkIn as string); + + let startDate: string; + let endDate: string; + + if (isAlldayLodging) { + // For all-day, use date part only, no timezone conversion + startDate = (checkIn as string).split('T')[0]; + + const endDateObj = new Date(checkOut as string); + endDateObj.setDate(endDateObj.getDate()); + endDate = endDateObj.toISOString().split('T')[0]; + + return { + id: lodging.id, + start: startDate, + end: endDate, + title: `${getLodgingIcon(lodging.type)} ${lodging.name}`, + backgroundColor: '#f59e0b' + }; + } else { + // Only use timezone if not all-day + const lodgingTimezone = lodging.timezone || userTimezone; + const checkInDateTime = new Date(checkIn as string); + const checkOutDateTime = new Date(checkOut as string); + + startDate = new Intl.DateTimeFormat('sv-SE', { + timeZone: lodgingTimezone, + year: 'numeric', + month: '2-digit', + day: '2-digit' + }).format(checkInDateTime); + + endDate = new Intl.DateTimeFormat('sv-SE', { + timeZone: lodgingTimezone, + year: 'numeric', + month: '2-digit', + day: '2-digit' + }).format(checkOutDateTime); + + return { + id: lodging.id, + start: startDate, + end: endDate, + title: lodging.name, + backgroundColor: '#f59e0b' + }; + } + }) + .filter((item) => item !== null) ); } @@ -994,7 +1042,7 @@ numberOfDays + 1 )[dateString] || []} {@const dayLodging = - groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays + 1)[ + groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays)[ dateString ] || []} {@const dayNotes = From df24316837af3eae91d7fe1eee2ea6e01427d2d4 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 18 Jun 2025 21:10:10 -0400 Subject: [PATCH 04/10] feat(lodging): improve lodging date handling with all-day event support and timezone adjustments --- .../src/lib/components/LodgingCard.svelte | 48 ++++-- .../src/lib/components/LodgingModal.svelte | 19 +++ .../lib/components/TransportationCard.svelte | 23 ++- frontend/src/lib/index.ts | 156 ++++++++++++------ .../src/routes/collections/[id]/+page.svelte | 2 +- 5 files changed, 170 insertions(+), 78 deletions(-) diff --git a/frontend/src/lib/components/LodgingCard.svelte b/frontend/src/lib/components/LodgingCard.svelte index fe27bff..d4a07a3 100644 --- a/frontend/src/lib/components/LodgingCard.svelte +++ b/frontend/src/lib/components/LodgingCard.svelte @@ -120,23 +120,39 @@
{/if} - {#if lodging.check_in && lodging.check_out} -
- {$t('adventures.dates')}: -

- {#if isAllDay(lodging.check_in)} - {formatAllDayDate(lodging.check_in)} – - {formatAllDayDate(lodging.check_out)} - {:else} - {formatDateInTimezone(lodging.check_in, lodging.timezone)} – - {formatDateInTimezone(lodging.check_out, lodging.timezone)} - {#if lodging.timezone} - ({lodging.timezone}) +

+ {#if lodging.check_in} +
+ {$t('adventures.check_in')}: + + {#if isAllDay(lodging.check_in)} + {formatAllDayDate(lodging.check_in)} + {:else} + {formatDateInTimezone(lodging.check_in, lodging.timezone)} + {#if lodging.timezone} + ({lodging.timezone}) + {/if} {/if} - {/if} -

-
- {/if} + +
+ {/if} + + {#if lodging.check_out} +
+ {$t('adventures.check_out')}: + + {#if isAllDay(lodging.check_out)} + {formatAllDayDate(lodging.check_out)} + {:else} + {formatDateInTimezone(lodging.check_out, lodging.timezone)} + {#if lodging.timezone} + ({lodging.timezone}) + {/if} + {/if} + +
+ {/if} +
diff --git a/frontend/src/lib/components/LodgingModal.svelte b/frontend/src/lib/components/LodgingModal.svelte index 594d505..5db3818 100644 --- a/frontend/src/lib/components/LodgingModal.svelte +++ b/frontend/src/lib/components/LodgingModal.svelte @@ -6,6 +6,7 @@ import type { Collection, Lodging } from '$lib/types'; import LocationDropdown from './LocationDropdown.svelte'; import DateRangeCollapse from './DateRangeCollapse.svelte'; + import { isAllDay } from '$lib'; const dispatch = createEventDispatcher(); @@ -82,6 +83,24 @@ async function handleSubmit(event: Event) { event.preventDefault(); + lodging.timezone = lodgingTimezone || null; + + // Auto-set end date if missing but start date exists + if (lodging.check_in && !lodging.check_out) { + const startDate = new Date(lodging.check_in); + const nextDay = new Date(startDate); + nextDay.setDate(nextDay.getDate() + 1); + + if (isAllDay(lodging.check_in)) { + // For all-day, set to next day at 00:00:00 + lodging.check_out = nextDay.toISOString().split('T')[0] + 'T00:00:00'; + } else { + // For timed events, set to next day at 9:00 AM + nextDay.setHours(9, 0, 0, 0); + lodging.check_out = nextDay.toISOString(); + } + } + // Create or update lodging... const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`; const method = lodging.id === '' ? 'POST' : 'PATCH'; diff --git a/frontend/src/lib/components/TransportationCard.svelte b/frontend/src/lib/components/TransportationCard.svelte index 16dd996..f419e34 100644 --- a/frontend/src/lib/components/TransportationCard.svelte +++ b/frontend/src/lib/components/TransportationCard.svelte @@ -8,7 +8,8 @@ import DeleteWarning from './DeleteWarning.svelte'; // import ArrowDownThick from '~icons/mdi/arrow-down-thick'; import { TRANSPORTATION_TYPES_ICONS } from '$lib'; - import { formatDateInTimezone } from '$lib/dateUtils'; + import { formatAllDayDate, formatDateInTimezone } from '$lib/dateUtils'; + import { isAllDay } from '$lib'; function getTransportationIcon(type: string) { if (type in TRANSPORTATION_TYPES_ICONS) { @@ -161,9 +162,13 @@
{$t('adventures.start')}: - {formatDateInTimezone(transportation.date, transportation.start_timezone)} - {#if transportation.start_timezone} - ({transportation.start_timezone}) + {#if isAllDay(transportation.date) && (!transportation.end_date || isAllDay(transportation.end_date))} + {formatAllDayDate(transportation.date)} + {:else} + {formatDateInTimezone(transportation.date, transportation.start_timezone)} + {#if transportation.start_timezone} + ({transportation.start_timezone}) + {/if} {/if}
@@ -173,9 +178,13 @@
{$t('adventures.end')}: - {formatDateInTimezone(transportation.end_date, transportation.end_timezone)} - {#if transportation.end_timezone} - ({transportation.end_timezone}) + {#if isAllDay(transportation.end_date) && (!transportation.date || isAllDay(transportation.date))} + {formatAllDayDate(transportation.end_date)} + {:else} + {formatDateInTimezone(transportation.end_date, transportation.end_timezone)} + {#if transportation.end_timezone} + ({transportation.end_timezone}) + {/if} {/if}
diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index f4f74dc..4f890e8 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -1,5 +1,8 @@ import inspirationalQuotes from './json/quotes.json'; import randomBackgrounds from './json/backgrounds.json'; + +// @ts-ignore +import { DateTime } from 'luxon'; import type { Adventure, Background, @@ -144,13 +147,6 @@ function getLocalDateString(date: Date): string { return `${year}-${month}-${day}`; } -// Helper to check if a given date string represents midnight (all-day) -// Improved isAllDay function to handle different ISO date formats -export function isAllDay(dateStr: string): boolean { - // Check for various midnight formats in UTC - return dateStr.endsWith('T00:00:00Z') || dateStr.endsWith('T00:00:00.000Z'); -} - export function groupTransportationsByDate( transportations: Transportation[], startDate: Date, @@ -158,82 +154,127 @@ export function groupTransportationsByDate( ): Record { const groupedTransportations: Record = {}; - // Initialize all days in the range + // Initialize days for (let i = 0; i < numberOfDays; i++) { - const currentDate = new Date(startDate); - currentDate.setDate(startDate.getDate() + i); - const dateString = getLocalDateString(currentDate); + const currentDate = DateTime.fromJSDate(startDate).plus({ days: i }); + const dateString = currentDate.toISODate(); // 'YYYY-MM-DD' groupedTransportations[dateString] = []; } transportations.forEach((transportation) => { if (transportation.date) { - const transportationDate = getLocalDateString(new Date(transportation.date)); - if (transportation.end_date) { - const endDate = new Date(transportation.end_date).toISOString().split('T')[0]; + // Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00) + const startHasZeros = transportation.date.includes('T00:00:00'); + const endHasZeros = transportation.end_date + ? transportation.end_date.includes('T00:00:00') + : true; + const isTranspoAllDay = startHasZeros && endHasZeros; - // Loop through all days and include transportation if it falls within the range - for (let i = 0; i < numberOfDays; i++) { - const currentDate = new Date(startDate); - currentDate.setDate(startDate.getDate() + i); - const dateString = getLocalDateString(currentDate); + let startDT: DateTime; + let endDT: DateTime; - // Include the current day if it falls within the transportation date range - if (dateString >= transportationDate && dateString <= endDate) { - if (groupedTransportations[dateString]) { - groupedTransportations[dateString].push(transportation); - } - } + if (isTranspoAllDay) { + // For all-day events, extract just the date part and ignore timezone + const dateOnly = transportation.date.split('T')[0]; // Get 'YYYY-MM-DD' + startDT = DateTime.fromISO(dateOnly); // This creates a date without time/timezone + + endDT = transportation.end_date + ? DateTime.fromISO(transportation.end_date.split('T')[0]) + : startDT; + } else { + // For timed events, use timezone conversion + startDT = DateTime.fromISO(transportation.date, { + zone: transportation.start_timezone ?? 'UTC' + }); + + endDT = transportation.end_date + ? DateTime.fromISO(transportation.end_date, { + zone: transportation.end_timezone ?? transportation.start_timezone ?? 'UTC' + }) + : startDT; + } + + const startDateStr = startDT.toISODate(); + const endDateStr = endDT.toISODate(); + + // Loop through all days in range + for (let i = 0; i < numberOfDays; i++) { + const currentDate = DateTime.fromJSDate(startDate).plus({ days: i }); + const currentDateStr = currentDate.toISODate(); + + if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) { + groupedTransportations[currentDateStr]?.push(transportation); } - } else if (groupedTransportations[transportationDate]) { - // If there's no end date, add transportation to the start date only - groupedTransportations[transportationDate].push(transportation); } } }); return groupedTransportations; } - export function groupLodgingByDate( - transportations: Lodging[], + lodging: Lodging[], startDate: Date, numberOfDays: number ): Record { - const groupedTransportations: Record = {}; + const groupedLodging: Record = {}; - // Initialize all days in the range using local dates - for (let i = 0; i < numberOfDays; i++) { - const currentDate = new Date(startDate); - currentDate.setDate(startDate.getDate() + i); - const dateString = getLocalDateString(currentDate); - groupedTransportations[dateString] = []; + // Initialize days (excluding last day for lodging) + // If trip is 7/1 to 7/4 (4 days), show lodging only on 7/1, 7/2, 7/3 + const lodgingDays = numberOfDays - 1; + + for (let i = 0; i < lodgingDays; i++) { + const currentDate = DateTime.fromJSDate(startDate).plus({ days: i }); + const dateString = currentDate.toISODate(); // 'YYYY-MM-DD' + groupedLodging[dateString] = []; } - transportations.forEach((transportation) => { - if (transportation.check_in) { - // Use local date string conversion - const transportationDate = getLocalDateString(new Date(transportation.check_in)); - if (transportation.check_out) { - const endDate = getLocalDateString(new Date(transportation.check_out)); + lodging.forEach((hotel) => { + if (hotel.check_in) { + // Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00) + const startHasZeros = hotel.check_in.includes('T00:00:00'); + const endHasZeros = hotel.check_out ? hotel.check_out.includes('T00:00:00') : true; + const isAllDay = startHasZeros && endHasZeros; - // Loop through all days and include transportation if it falls within the transportation date range - for (let i = 0; i < numberOfDays; i++) { - const currentDate = new Date(startDate); - currentDate.setDate(startDate.getDate() + i); - const dateString = getLocalDateString(currentDate); + let startDT: DateTime; + let endDT: DateTime; - if (dateString >= transportationDate && dateString <= endDate) { - groupedTransportations[dateString].push(transportation); - } + if (isAllDay) { + // For all-day events, extract just the date part and ignore timezone + const dateOnly = hotel.check_in.split('T')[0]; // Get 'YYYY-MM-DD' + startDT = DateTime.fromISO(dateOnly); // This creates a date without time/timezone + + endDT = hotel.check_out ? DateTime.fromISO(hotel.check_out.split('T')[0]) : startDT; + } else { + // For timed events, use timezone conversion + startDT = DateTime.fromISO(hotel.check_in, { + zone: hotel.timezone ?? 'UTC' + }); + + endDT = hotel.check_out + ? DateTime.fromISO(hotel.check_out, { + zone: hotel.timezone ?? 'UTC' + }) + : startDT; + } + + const startDateStr = startDT.toISODate(); + const endDateStr = endDT.toISODate(); + + // Loop through lodging days only (excluding last day) + for (let i = 0; i < lodgingDays; i++) { + const currentDate = DateTime.fromJSDate(startDate).plus({ days: i }); + const currentDateStr = currentDate.toISODate(); + + // Show lodging on days where check-in occurs through the day before check-out + // For lodging, we typically want to show it on the nights you're staying + if (currentDateStr >= startDateStr && currentDateStr < endDateStr) { + groupedLodging[currentDateStr]?.push(hotel); } - } else if (groupedTransportations[transportationDate]) { - groupedTransportations[transportationDate].push(transportation); } } }); - return groupedTransportations; + return groupedLodging; } export function groupNotesByDate( @@ -292,6 +333,13 @@ export function groupChecklistsByDate( return groupedChecklists; } +// Helper to check if a given date string represents midnight (all-day) +// Improved isAllDay function to handle different ISO date formats +export function isAllDay(dateStr: string): boolean { + // Check for various midnight formats in UTC + return dateStr.endsWith('T00:00:00Z') || dateStr.endsWith('T00:00:00.000Z'); +} + export function continentCodeToString(code: string) { switch (code) { case 'AF': diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index cca5d6b..cd75934 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -1042,7 +1042,7 @@ numberOfDays + 1 )[dateString] || []} {@const dayLodging = - groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays)[ + groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays + 1)[ dateString ] || []} {@const dayNotes = From 9964398e259a044679f22fffa9b9dafa103d659c Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 18 Jun 2025 22:21:34 -0400 Subject: [PATCH 05/10] feat(lodging): add check-in and check-out labels and enhance date handling for lodging events --- .../lib/components/DateRangeCollapse.svelte | 10 ++- .../src/lib/components/LodgingModal.svelte | 22 +++-- frontend/src/lib/index.ts | 83 +++++++++---------- frontend/src/locales/de.json | 4 +- frontend/src/locales/en.json | 2 + frontend/src/locales/es.json | 4 +- frontend/src/locales/fr.json | 4 +- frontend/src/locales/it.json | 4 +- frontend/src/locales/ko.json | 4 +- frontend/src/locales/nl.json | 4 +- frontend/src/locales/no.json | 4 +- frontend/src/locales/pl.json | 4 +- frontend/src/locales/ru.json | 4 +- frontend/src/locales/sv.json | 4 +- frontend/src/locales/zh.json | 4 +- 15 files changed, 93 insertions(+), 68 deletions(-) diff --git a/frontend/src/lib/components/DateRangeCollapse.svelte b/frontend/src/lib/components/DateRangeCollapse.svelte index 7ff595f..eeec650 100644 --- a/frontend/src/lib/components/DateRangeCollapse.svelte +++ b/frontend/src/lib/components/DateRangeCollapse.svelte @@ -263,7 +263,9 @@ {#if allDay} @@ -295,7 +297,11 @@ {#if localStartDate}
{#if allDay} diff --git a/frontend/src/lib/components/LodgingModal.svelte b/frontend/src/lib/components/LodgingModal.svelte index 5db3818..6eb97a2 100644 --- a/frontend/src/lib/components/LodgingModal.svelte +++ b/frontend/src/lib/components/LodgingModal.svelte @@ -7,6 +7,8 @@ import LocationDropdown from './LocationDropdown.svelte'; import DateRangeCollapse from './DateRangeCollapse.svelte'; import { isAllDay } from '$lib'; + // @ts-ignore + import { DateTime } from 'luxon'; const dispatch = createEventDispatcher(); @@ -85,19 +87,21 @@ lodging.timezone = lodgingTimezone || null; + console.log(lodgingTimezone); + // Auto-set end date if missing but start date exists if (lodging.check_in && !lodging.check_out) { - const startDate = new Date(lodging.check_in); - const nextDay = new Date(startDate); - nextDay.setDate(nextDay.getDate() + 1); - if (isAllDay(lodging.check_in)) { - // For all-day, set to next day at 00:00:00 - lodging.check_out = nextDay.toISOString().split('T')[0] + 'T00:00:00'; + // For all-day, just add one day and keep at UTC 00:00:00 + const start = DateTime.fromISO(lodging.check_in, { zone: 'utc' }); + const nextDay = start.plus({ days: 1 }); + lodging.check_out = nextDay.toISO(); } else { - // For timed events, set to next day at 9:00 AM - nextDay.setHours(9, 0, 0, 0); - lodging.check_out = nextDay.toISOString(); + // For timed events, set to next day at 9:00 AM in lodging's timezone, then convert to UTC + const start = DateTime.fromISO(lodging.check_in, { zone: lodging.timezone || 'utc' }); + const nextDay = start.plus({ days: 1 }); + const end = nextDay.set({ hour: 9, minute: 0, second: 0, millisecond: 0 }); + lodging.check_out = end.toUTC().toISO(); } } diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index 4f890e8..21bee80 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -70,66 +70,57 @@ export function groupAdventuresByDate( ): Record { const groupedAdventures: Record = {}; - // Initialize all days in the range + // Initialize all days in the range using DateTime for (let i = 0; i < numberOfDays; i++) { - const currentDate = new Date(startDate); - currentDate.setDate(startDate.getDate() + i); - const dateString = getLocalDateString(currentDate); + const currentDate = DateTime.fromJSDate(startDate).plus({ days: i }); + const dateString = currentDate.toISODate(); // 'YYYY-MM-DD' groupedAdventures[dateString] = []; } adventures.forEach((adventure) => { adventure.visits.forEach((visit) => { if (visit.start_date) { - // Check if this is an all-day event (both start and end at midnight) - const isAllDayEvent = - isAllDay(visit.start_date) && (visit.end_date ? isAllDay(visit.end_date) : false); + // Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00) + const startHasZeros = visit.start_date.includes('T00:00:00'); + const endHasZeros = visit.end_date ? visit.end_date.includes('T00:00:00') : true; + const isAllDayEvent = startHasZeros && endHasZeros; - // For all-day events, we need to handle dates differently - if (isAllDayEvent && visit.end_date) { - // Extract just the date parts without time - const startDateStr = visit.start_date.split('T')[0]; - const endDateStr = visit.end_date.split('T')[0]; + let startDT: DateTime; + let endDT: DateTime; - // Loop through all days in the range - for (let i = 0; i < numberOfDays; i++) { - const currentDate = new Date(startDate); - currentDate.setDate(startDate.getDate() + i); - const currentDateStr = getLocalDateString(currentDate); + if (isAllDayEvent) { + // For all-day events, extract just the date part and ignore timezone + const dateOnly = visit.start_date.split('T')[0]; // Get 'YYYY-MM-DD' + startDT = DateTime.fromISO(dateOnly); // This creates a date without time/timezone - // Include the current day if it falls within the adventure date range - if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) { - if (groupedAdventures[currentDateStr]) { - groupedAdventures[currentDateStr].push(adventure); - } - } - } + endDT = visit.end_date + ? DateTime.fromISO(visit.end_date.split('T')[0]) + : startDT; } else { - // Handle regular events with time components - const adventureStartDate = new Date(visit.start_date); - const adventureDateStr = getLocalDateString(adventureStartDate); + // For timed events, use timezone conversion + startDT = DateTime.fromISO(visit.start_date, { + zone: visit.timezone ?? 'UTC' + }); - if (visit.end_date) { - const adventureEndDate = new Date(visit.end_date); - const endDateStr = getLocalDateString(adventureEndDate); + endDT = visit.end_date + ? DateTime.fromISO(visit.end_date, { + zone: visit.timezone ?? 'UTC' + }) + : startDT; + } - // Loop through all days and include adventure if it falls within the range - for (let i = 0; i < numberOfDays; i++) { - const currentDate = new Date(startDate); - currentDate.setDate(startDate.getDate() + i); - const dateString = getLocalDateString(currentDate); + const startDateStr = startDT.toISODate(); + const endDateStr = endDT.toISODate(); - // Include the current day if it falls within the adventure date range - if (dateString >= adventureDateStr && dateString <= endDateStr) { - if (groupedAdventures[dateString]) { - groupedAdventures[dateString].push(adventure); - } - } - } - } else { - // If there's no end date, add adventure to the start date only - if (groupedAdventures[adventureDateStr]) { - groupedAdventures[adventureDateStr].push(adventure); + // Loop through all days in range + for (let i = 0; i < numberOfDays; i++) { + const currentDate = DateTime.fromJSDate(startDate).plus({ days: i }); + const currentDateStr = currentDate.toISODate(); + + // Include the current day if it falls within the adventure date range + if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) { + if (groupedAdventures[currentDateStr]) { + groupedAdventures[currentDateStr].push(adventure); } } } diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 33308ba..c007da1 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -244,7 +244,9 @@ "done": "Erledigt", "loading_adventures": "Ladeabenteuer ...", "name_location": "Name, Ort", - "collection_contents": "Sammelinhalt" + "collection_contents": "Sammelinhalt", + "check_in": "Einchecken", + "check_out": "Kasse" }, "home": { "desc_1": "Entdecken, planen und erkunden Sie mühelos", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 82e2fa4..31c0614 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -98,6 +98,8 @@ "latitude": "Latitude", "visit": "Visit", "timed": "Timed", + "check_in": "Check In", + "check_out": "Check Out", "coordinates": "Coordinates", "copy_coordinates": "Copy Coordinates", "visits": "Visits", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 61f54e7..fcf8502 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -296,7 +296,9 @@ "done": "Hecho", "loading_adventures": "Cargando aventuras ...", "name_location": "Nombre, ubicación", - "collection_contents": "Contenido de la colección" + "collection_contents": "Contenido de la colección", + "check_in": "Registrarse", + "check_out": "Verificar" }, "worldtravel": { "all": "Todo", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index c8412ce..9e61eb8 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -244,7 +244,9 @@ "done": "Fait", "loading_adventures": "Chargement des aventures ...", "name_location": "nom, emplacement", - "collection_contents": "Contenu de la collection" + "collection_contents": "Contenu de la collection", + "check_in": "Enregistrement", + "check_out": "Vérifier" }, "home": { "desc_1": "Découvrez, planifiez et explorez en toute simplicité", diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 9a1060b..ce522e0 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -244,7 +244,9 @@ "done": "Fatto", "loading_adventures": "Caricamento di avventure ...", "name_location": "Nome, posizione", - "collection_contents": "Contenuto di raccolta" + "collection_contents": "Contenuto di raccolta", + "check_in": "Check -in", + "check_out": "Guardare" }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json index 3b2cd2d..f23fc7b 100644 --- a/frontend/src/locales/ko.json +++ b/frontend/src/locales/ko.json @@ -244,7 +244,9 @@ "done": "완료", "loading_adventures": "적재 모험 ...", "name_location": "이름, 위치", - "collection_contents": "수집 내용" + "collection_contents": "수집 내용", + "check_in": "체크인", + "check_out": "체크 아웃" }, "auth": { "confirm_password": "비밀번호 확인", diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index e060694..ec06221 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -244,7 +244,9 @@ "done": "Klaar", "loading_adventures": "Adventuren laden ...", "name_location": "naam, locatie", - "collection_contents": "Verzamelingsinhoud" + "collection_contents": "Verzamelingsinhoud", + "check_in": "Inchecken", + "check_out": "Uitchecken" }, "home": { "desc_1": "Ontdek, plan en verken met gemak", diff --git a/frontend/src/locales/no.json b/frontend/src/locales/no.json index b703275..b3b3539 100644 --- a/frontend/src/locales/no.json +++ b/frontend/src/locales/no.json @@ -296,7 +296,9 @@ "done": "Ferdig", "loading_adventures": "Laster opp eventyr ...", "name_location": "Navn, plassering", - "collection_contents": "Samlingsinnhold" + "collection_contents": "Samlingsinnhold", + "check_in": "Sjekk inn", + "check_out": "Sjekk ut" }, "worldtravel": { "country_list": "Liste over land", diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index 51c0324..af72437 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -296,7 +296,9 @@ "loading_adventures": "Ładowanie przygód ...", "name_location": "Nazwa, lokalizacja", "delete_collection_warning": "Czy na pewno chcesz usunąć tę kolekcję? \nTego działania nie można cofnąć.", - "collection_contents": "Zawartość kolekcji" + "collection_contents": "Zawartość kolekcji", + "check_in": "Zameldować się", + "check_out": "Wymeldować się" }, "worldtravel": { "country_list": "Lista krajów", diff --git a/frontend/src/locales/ru.json b/frontend/src/locales/ru.json index 149d2f1..7d063fe 100644 --- a/frontend/src/locales/ru.json +++ b/frontend/src/locales/ru.json @@ -296,7 +296,9 @@ "done": "Сделанный", "loading_adventures": "Загрузка приключений ...", "name_location": "имя, местоположение", - "collection_contents": "Содержание коллекции" + "collection_contents": "Содержание коллекции", + "check_in": "Регистрироваться", + "check_out": "Проверить" }, "worldtravel": { "country_list": "Список стран", diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 309b984..87d747c 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -244,7 +244,9 @@ "done": "Gjort", "loading_adventures": "Laddar äventyr ...", "name_location": "namn, plats", - "collection_contents": "Insamlingsinnehåll" + "collection_contents": "Insamlingsinnehåll", + "check_in": "Checka in", + "check_out": "Checka ut" }, "home": { "desc_1": "Upptäck, planera och utforska med lätthet", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index f111b07..55a98ce 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -296,7 +296,9 @@ "done": "完毕", "loading_adventures": "加载冒险...", "name_location": "名称,位置", - "collection_contents": "收集内容" + "collection_contents": "收集内容", + "check_in": "报到", + "check_out": "查看" }, "auth": { "forgot_password": "忘记密码?", From 3b0ccdb6d3c0637421da88554cc31da294c22eb0 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 18 Jun 2025 22:29:37 -0400 Subject: [PATCH 06/10] feat(DateRangeCollapse): auto-detect all-day setting for transportation and lodging types --- frontend/src/lib/components/DateRangeCollapse.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/lib/components/DateRangeCollapse.svelte b/frontend/src/lib/components/DateRangeCollapse.svelte index eeec650..4a72365 100644 --- a/frontend/src/lib/components/DateRangeCollapse.svelte +++ b/frontend/src/lib/components/DateRangeCollapse.svelte @@ -51,6 +51,11 @@ let isEditing = false; // Disable reactivity when editing onMount(async () => { + // Auto-detect all-day for transportation and lodging types + if ((type === 'transportation' || type === 'lodging') && utcStartDate) { + allDay = isAllDay(utcStartDate); + } + // Initialize UTC dates localStartDate = updateLocalDate({ utcDate: utcStartDate, From 36f902287221be0a70993ed6fd9153e48445db03 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 19 Jun 2025 11:33:04 -0400 Subject: [PATCH 07/10] fix(lodging): remove console log and improve all-day event checks in lodging modal --- frontend/src/lib/components/LodgingModal.svelte | 2 -- frontend/src/lib/config.ts | 2 +- frontend/src/lib/index.ts | 8 +++----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/components/LodgingModal.svelte b/frontend/src/lib/components/LodgingModal.svelte index 6eb97a2..c7a3fd9 100644 --- a/frontend/src/lib/components/LodgingModal.svelte +++ b/frontend/src/lib/components/LodgingModal.svelte @@ -87,8 +87,6 @@ lodging.timezone = lodgingTimezone || null; - console.log(lodgingTimezone); - // Auto-set end date if missing but start date exists if (lodging.check_in && !lodging.check_out) { if (isAllDay(lodging.check_in)) { diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index a9893c2..6772b0a 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -1,4 +1,4 @@ -export let appVersion = 'v0.10.0-main-06182025'; +export let appVersion = 'v0.10.0-main-06192025'; export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.10.0'; export let appTitle = 'AdventureLog'; export let copyrightYear = '2023-2025'; diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index 21bee80..18549a6 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -81,8 +81,8 @@ export function groupAdventuresByDate( adventure.visits.forEach((visit) => { if (visit.start_date) { // Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00) - const startHasZeros = visit.start_date.includes('T00:00:00'); - const endHasZeros = visit.end_date ? visit.end_date.includes('T00:00:00') : true; + const startHasZeros = isAllDay(visit.start_date); + const endHasZeros = visit.end_date ? isAllDay(visit.end_date) : true; const isAllDayEvent = startHasZeros && endHasZeros; let startDT: DateTime; @@ -93,9 +93,7 @@ export function groupAdventuresByDate( const dateOnly = visit.start_date.split('T')[0]; // Get 'YYYY-MM-DD' startDT = DateTime.fromISO(dateOnly); // This creates a date without time/timezone - endDT = visit.end_date - ? DateTime.fromISO(visit.end_date.split('T')[0]) - : startDT; + endDT = visit.end_date ? DateTime.fromISO(visit.end_date.split('T')[0]) : startDT; } else { // For timed events, use timezone conversion startDT = DateTime.fromISO(visit.start_date, { From a6b39f64d68fc2fa66fca398b8146c50d319ba61 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 19 Jun 2025 11:37:08 -0400 Subject: [PATCH 08/10] feat(calendar): add adventure detail link in event modal --- frontend/src/routes/calendar/+page.server.ts | 4 +++- frontend/src/routes/calendar/+page.svelte | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/src/routes/calendar/+page.server.ts b/frontend/src/routes/calendar/+page.server.ts index 0c46d7d..383bd0a 100644 --- a/frontend/src/routes/calendar/+page.server.ts +++ b/frontend/src/routes/calendar/+page.server.ts @@ -34,6 +34,7 @@ export const load = (async (event) => { formattedEnd: string; location?: string; description?: string; + adventureId?: string; }; }> = []; @@ -119,7 +120,8 @@ export const load = (async (event) => { ? formatAllDayDate(visit.end_date || visit.start_date) : formatDateInTimezone(visit.end_date || visit.start_date, targetTimezone), location: adventure.location || '', - description: adventure.description || '' + description: adventure.description || '', + adventureId: adventure.id } }); } diff --git a/frontend/src/routes/calendar/+page.svelte b/frontend/src/routes/calendar/+page.svelte index 148f94a..0d047d1 100644 --- a/frontend/src/routes/calendar/+page.svelte +++ b/frontend/src/routes/calendar/+page.svelte @@ -408,6 +408,15 @@
{/if} + + {#if selectedEvent.extendedProps.adventureId} + + {$t('map.view_details')} + + {/if}