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