From 9fd2a142cbcb1325099b0f436952f17d1757c94f Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 18 Mar 2025 14:04:31 -0400 Subject: [PATCH 01/10] feat: Update Visit model to use DateTimeField for start and end dates, and enhance AdventureModal with datetime-local inputs --- ...r_visit_end_date_alter_visit_start_date.py | 23 ++++ backend/server/adventures/models.py | 4 +- backend/server/adventures/serializers.py | 6 +- .../src/lib/components/AdventureModal.svelte | 107 +++++++++++++++--- frontend/src/lib/index.ts | 6 + .../src/routes/adventures/[id]/+page.svelte | 41 ++++--- 6 files changed, 152 insertions(+), 35 deletions(-) create mode 100644 backend/server/adventures/migrations/0025_alter_visit_end_date_alter_visit_start_date.py diff --git a/backend/server/adventures/migrations/0025_alter_visit_end_date_alter_visit_start_date.py b/backend/server/adventures/migrations/0025_alter_visit_end_date_alter_visit_start_date.py new file mode 100644 index 00000000..668e9681 --- /dev/null +++ b/backend/server/adventures/migrations/0025_alter_visit_end_date_alter_visit_start_date.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.8 on 2025-03-17 21:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0024_alter_attachment_file'), + ] + + operations = [ + migrations.AlterField( + model_name='visit', + name='end_date', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='visit', + name='start_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index c7f78ca1..0d53bc94 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -76,8 +76,8 @@ User = get_user_model() class Visit(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits') - start_date = models.DateField(null=True, blank=True) - end_date = models.DateField(null=True, blank=True) + start_date = models.DateTimeField(null=True, blank=True) + end_date = models.DateTimeField(null=True, blank=True) notes = models.TextField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 97dd6339..d69466d6 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -136,9 +136,11 @@ class AdventureSerializer(CustomModelSerializer): def get_is_visited(self, obj): current_date = timezone.now().date() for visit in obj.visits.all(): - if visit.start_date and visit.end_date and (visit.start_date <= current_date): + start_date = visit.start_date.date() if isinstance(visit.start_date, timezone.datetime) else visit.start_date + end_date = visit.end_date.date() if isinstance(visit.end_date, timezone.datetime) else visit.end_date + if start_date and end_date and (start_date <= current_date): return True - elif visit.start_date and not visit.end_date and (visit.start_date <= current_date): + elif start_date and not end_date and (start_date <= current_date): return True return False diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index c8b07431..b9daa0f2 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -6,6 +6,16 @@ import { t } from 'svelte-i18n'; export let collection: Collection | null = null; + let fullStartDate: string = ''; + let fullEndDate: string = ''; + let allDay: boolean = false; + + // Set full start and end dates from collection + if (collection && collection.start_date && collection.end_date) { + fullStartDate = `${collection.start_date}T00:00`; + fullEndDate = `${collection.end_date}T23:59`; + } + const dispatch = createEventDispatcher(); let images: { id: string; image: string; is_primary: boolean }[] = []; @@ -72,7 +82,7 @@ import ActivityComplete from './ActivityComplete.svelte'; import CategoryDropdown from './CategoryDropdown.svelte'; - import { findFirstValue } from '$lib'; + import { findFirstValue, isAllDay } from '$lib'; import MarkdownEditor from './MarkdownEditor.svelte'; import ImmichSelect from './ImmichSelect.svelte'; import Star from '~icons/mdi/star'; @@ -379,7 +389,10 @@ 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 isn’t provided, assume it’s the same as start. if (new_start_date && !new_end_date) { new_end_date = new_start_date; } @@ -391,15 +404,31 @@ 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: '' + id: '' // or generate an id as needed } ]; + + // Clear the input fields. new_start_date = ''; new_end_date = ''; new_notes = ''; @@ -669,13 +698,23 @@ on:change={() => (constrainDates = !constrainDates)} /> {/if} + All Day + (allDay = !allDay)} + />
- {#if !constrainDates} + {#if !allDay} { if (e.key === 'Enter') { @@ -685,10 +724,12 @@ }} /> { if (e.key === 'Enter') { e.preventDefault(); @@ -701,8 +742,8 @@ type="date" class="input input-bordered w-full" placeholder={$t('adventures.start_date')} - min={collection?.start_date} - max={collection?.end_date} + min={constrainDates ? fullStartDate : ''} + max={constrainDates ? fullEndDate : ''} bind:value={new_start_date} on:keydown={(e) => { if (e.key === 'Enter') { @@ -716,8 +757,8 @@ class="input input-bordered w-full" placeholder={$t('adventures.end_date')} bind:value={new_end_date} - min={collection?.start_date} - max={collection?.end_date} + min={constrainDates ? fullStartDate : ''} + max={constrainDates ? fullEndDate : ''} on:keydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -742,6 +783,30 @@ >
+ +
{#if adventure.visits.length > 0} -

{$t('adventures.my_visits')}

+

{$t('adventures.my_visits')}

{#each adventure.visits as visit}
-
+

- {new Date(visit.start_date).toLocaleDateString(undefined, { - timeZone: 'UTC' - })} + {#if isAllDay(visit.start_date)} + + {new Date(visit.start_date).toLocaleDateString(undefined, { + timeZone: 'UTC' + })} + {:else} + + {new Date(visit.start_date).toLocaleDateString()} ({new Date( + visit.start_date + ).toLocaleTimeString()}) + {/if}

{#if visit.end_date && visit.end_date !== visit.start_date}

{new Date(visit.end_date).toLocaleDateString(undefined, { timeZone: 'UTC' })} + {#if !isAllDay(visit.end_date)} + ({new Date(visit.end_date).toLocaleTimeString()}) + {/if}

{/if} -
diff --git a/frontend/src/lib/components/TransportationModal.svelte b/frontend/src/lib/components/TransportationModal.svelte index 9012dd91..1d4a5161 100644 --- a/frontend/src/lib/components/TransportationModal.svelte +++ b/frontend/src/lib/components/TransportationModal.svelte @@ -16,10 +16,15 @@ let constrainDates: boolean = false; + // Format date as local datetime + // Convert an ISO date to a datetime-local value in local time. function toLocalDatetime(value: string | null): string { if (!value) return ''; const date = new Date(value); - return date.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:mm + // Adjust the time by subtracting the timezone offset. + date.setMinutes(date.getMinutes() - date.getTimezoneOffset()); + // Return format YYYY-MM-DDTHH:mm + return date.toISOString().slice(0, 16); } let transportation: Transportation = { @@ -185,6 +190,14 @@ return; } + // Convert local dates to UTC + if (transportation.date && !transportation.date.includes('Z')) { + transportation.date = new Date(transportation.date).toISOString(); + } + if (transportation.end_date && !transportation.end_date.includes('Z')) { + transportation.end_date = new Date(transportation.end_date).toISOString(); + } + if (transportation.type != 'plane') { transportation.flight_number = ''; } @@ -422,6 +435,29 @@
{/if} +
diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index fd215974..70aef59e 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -338,6 +338,17 @@ export let LODGING_TYPES_ICONS = { other: '❓' }; +export let TRANSPORTATION_TYPES_ICONS = { + car: '🚗', + plane: '✈️', + train: '🚆', + bus: '🚌', + boat: '⛵', + bike: '🚲', + walking: '🚶', + other: '❓' +}; + // Helper to check if a given date string represents midnight (all-day) export function isAllDay(dateStr: string | string[]) { // Checks for the pattern "T00:00:00.000Z" diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 56c84dcb..c0d939eb 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -247,7 +247,8 @@ "price": "Preis", "reservation_number": "Reservierungsnummer", "welcome_map_info": "Frei zugängliche Abenteuer auf diesem Server", - "open_in_maps": "In Karten öffnen" + "open_in_maps": "In Karten öffnen", + "all_day": "Den ganzen Tag" }, "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 25a79de8..4e32577b 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -250,6 +250,7 @@ "show_map": "Show Map", "emoji_picker": "Emoji Picker", "download_calendar": "Download Calendar", + "all_day": "All Day", "date_information": "Date Information", "flight_information": "Flight Information", "out_of_range": "Not in itinerary date range", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 3814ec2d..bd9b6a4e 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -295,7 +295,8 @@ "region": "Región", "reservation_number": "Número de reserva", "welcome_map_info": "Aventuras públicas en este servidor", - "open_in_maps": "Abrir en mapas" + "open_in_maps": "Abrir en mapas", + "all_day": "Todo el día" }, "worldtravel": { "all": "Todo", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index 3e6ec37d..2523fe5e 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -247,7 +247,8 @@ "region": "Région", "reservation_number": "Numéro de réservation", "welcome_map_info": "Aventures publiques sur ce serveur", - "open_in_maps": "Ouvert dans les cartes" + "open_in_maps": "Ouvert dans les cartes", + "all_day": "Toute la journée" }, "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 a68ce990..216d1210 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -247,7 +247,8 @@ "region": "Regione", "welcome_map_info": "Avventure pubbliche su questo server", "reservation_number": "Numero di prenotazione", - "open_in_maps": "Aperto in mappe" + "open_in_maps": "Aperto in mappe", + "all_day": "Tutto il giorno" }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json index bfe761b2..2099c0cf 100644 --- a/frontend/src/locales/ko.json +++ b/frontend/src/locales/ko.json @@ -247,7 +247,8 @@ "region": "지역", "reservation_number": "예약 번호", "welcome_map_info": "이 서버의 공개 모험", - "open_in_maps": "지도에서 열립니다" + "open_in_maps": "지도에서 열립니다", + "all_day": "하루 종일" }, "auth": { "both_passwords_required": "두 암호 모두 필요합니다", diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index 4a783ec0..6dbf2d78 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -247,7 +247,8 @@ "lodging_information": "Informatie overliggen", "price": "Prijs", "region": "Regio", - "open_in_maps": "Open in kaarten" + "open_in_maps": "Open in kaarten", + "all_day": "De hele dag" }, "home": { "desc_1": "Ontdek, plan en verken met gemak", diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index 3925de13..3eb73ac9 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -295,7 +295,8 @@ "region": "Region", "reservation_number": "Numer rezerwacji", "welcome_map_info": "Publiczne przygody na tym serwerze", - "open_in_maps": "Otwarte w mapach" + "open_in_maps": "Otwarte w mapach", + "all_day": "Cały dzień" }, "worldtravel": { "country_list": "Lista krajów", diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index c6fd2000..0baff34d 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -247,7 +247,8 @@ "price": "Pris", "region": "Område", "reservation_number": "Bokningsnummer", - "open_in_maps": "Kappas in" + "open_in_maps": "Kappas in", + "all_day": "Hela dagen" }, "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 864d6993..dfd5e8e0 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -295,7 +295,8 @@ "lodging_information": "住宿信息", "price": "价格", "reservation_number": "预订号", - "open_in_maps": "在地图上打开" + "open_in_maps": "在地图上打开", + "all_day": "整天" }, "auth": { "forgot_password": "忘记密码?", diff --git a/frontend/src/routes/adventures/[id]/+page.svelte b/frontend/src/routes/adventures/[id]/+page.svelte index 17f1ae35..6d88e0e4 100644 --- a/frontend/src/routes/adventures/[id]/+page.svelte +++ b/frontend/src/routes/adventures/[id]/+page.svelte @@ -456,12 +456,14 @@ {/if} - {$t('adventures.open_in_maps')} + {#if adventure.longitude && adventure.latitude} + {$t('adventures.open_in_maps')} + {/if} Date: Tue, 18 Mar 2025 18:16:25 -0400 Subject: [PATCH 03/10] feat: Implement chronological itinerary path visualization with GeoJSON for adventures, transportation, and lodging --- .../src/routes/collections/[id]/+page.svelte | 195 +++++++++++++++--- 1 file changed, 167 insertions(+), 28 deletions(-) diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index a63dbe5a..b6ffac3a 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -136,6 +136,66 @@ 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 + function createLineData( + items: Array<{ + item: Adventure | Transportation | Lodging | Note | Checklist; + start: string; + end: string; + }> + ) { + if (items.length < 2) return null; + + const coordinates: [number, number][] = []; + + // Extract coordinates from each item + for (const orderItem of items) { + const item = orderItem.item; + + if ( + 'origin_longitude' in item && + 'origin_latitude' in item && + 'destination_longitude' in item && + 'destination_latitude' in item && + item.origin_longitude && + item.origin_latitude && + item.destination_longitude && + item.destination_latitude + ) { + // For Transportation, add both origin and destination points + coordinates.push([item.origin_longitude, item.origin_latitude]); + coordinates.push([item.destination_longitude, item.destination_latitude]); + } else if ('longitude' in item && 'latitude' in item && item.longitude && item.latitude) { + // Handle Adventure and Lodging types + coordinates.push([item.longitude, item.latitude]); + } + } + + // Only create line data if we have at least 2 coordinates + if (coordinates.length >= 2) { + return { + type: 'Feature' as const, + properties: { + name: 'Itinerary Path', + description: 'Path connecting chronological items' + }, + geometry: { + type: 'LineString' as const, + coordinates: coordinates + } + }; + } + + return null; + } + let numVisited: number = 0; let numAdventures: number = 0; @@ -169,6 +229,63 @@ } } + let orderedItems: Array<{ + item: Adventure | Transportation | Lodging; + type: 'adventure' | 'transportation' | 'lodging'; + start: string; // ISO date string + end: string; // ISO date string + }> = []; + + $: { + // Reset ordered items + orderedItems = []; + + // Add Adventures (using visit dates) + adventures.forEach((adventure) => { + adventure.visits.forEach((visit) => { + orderedItems.push({ + item: adventure, + start: visit.start_date, + end: visit.end_date, + type: 'adventure' + }); + }); + }); + + // Add Transportation + transportations.forEach((transport) => { + if (transport.date) { + // Only add if date exists + orderedItems.push({ + item: transport, + start: transport.date, + end: transport.end_date || transport.date, // Use end_date if available, otherwise use date, + type: 'transportation' + }); + } + }); + + // Add Lodging + lodging.forEach((lodging) => { + if (lodging.check_in) { + // Only add if check_in exists + orderedItems.push({ + item: lodging, + start: lodging.check_in, + end: lodging.check_out || lodging.check_in, // Use check_out if available, otherwise use check_in, + type: 'lodging' + }); + } + }); + + // Sort all items chronologically by start date + orderedItems.sort((a, b) => { + const dateA = new Date(a.start).getTime(); + const dateB = new Date(b.start).getTime(); + return dateA - dateB; + }); + } + $: { numAdventures = adventures.length; numVisited = adventures.filter((adventure) => adventure.is_visited).length; @@ -994,6 +1111,19 @@ {/if} {/each} + {#if lineData} + + + + {/if} {#each transportations as transportation} {#if transportation.origin_latitude && transportation.origin_longitude && transportation.destination_latitude && transportation.destination_longitude} @@ -1035,34 +1165,6 @@

- - - - - {/if} {/each} @@ -1286,6 +1388,43 @@ {/if} {/if} +{#each orderedItems as orderedItem} +

{orderedItem.type}

+ {#if orderedItem.type === 'adventure'} + {#if orderedItem.item && 'images' in orderedItem.item} + + {/if} + {/if} + {#if orderedItem.type === 'transportation' && orderedItem.item && 'origin_latitude' in orderedItem.item} + { + transportations = transportations.filter((t) => t.id != event.detail); + }} + on:edit={editTransportation} + {collection} + /> + {/if} + {#if orderedItem.type === 'lodging' && orderedItem.item && 'reservation_number' in orderedItem.item} + { + lodging = lodging.filter((t) => t.id != event.detail); + }} + on:edit={editLodging} + {collection} + /> + {/if} +{/each} + {data.props.adventure && data.props.adventure.name From f554bb8777281772ea7fc8cec40e71ae8d59023e Mon Sep 17 00:00:00 2001 From: Sean Morley <zipsm15@gmail.com> Date: Tue, 18 Mar 2025 21:07:34 -0400 Subject: [PATCH 04/10] feat: Enhance date handling in AdventureModal and related components for improved localization and all-day event support --- .../src/lib/components/AdventureModal.svelte | 15 ++-- frontend/src/lib/index.ts | 72 ++++++++++--------- .../src/routes/collections/[id]/+page.svelte | 13 ++-- 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 1321db0d..3054687d 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -834,11 +834,16 @@ </p> {#if visit.end_date && visit.end_date !== visit.start_date} <p> - {new Date(visit.end_date).toLocaleDateString(undefined, { - timeZone: 'UTC' - })} - {#if !isAllDay(visit.end_date)} - ({new Date(visit.end_date).toLocaleTimeString()}) + {#if isAllDay(visit.end_date)} + <!-- For all-day events, show just the date --> + {new Date(visit.end_date).toLocaleDateString(undefined, { + timeZone: 'UTC' + })} + {:else} + <!-- For timed events, show date and time --> + {new Date(visit.end_date).toLocaleDateString()} ({new Date( + visit.end_date + ).toLocaleTimeString()}) {/if} </p> {/if} diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index 70aef59e..af68de26 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -70,23 +70,23 @@ export function groupAdventuresByDate( // Initialize all days in the range for (let i = 0; i < numberOfDays; i++) { const currentDate = new Date(startDate); - currentDate.setUTCDate(startDate.getUTCDate() + i); - const dateString = currentDate.toISOString().split('T')[0]; + currentDate.setDate(startDate.getDate() + i); + const dateString = getLocalDateString(currentDate); groupedAdventures[dateString] = []; } adventures.forEach((adventure) => { adventure.visits.forEach((visit) => { if (visit.start_date) { - const adventureDate = new Date(visit.start_date).toISOString().split('T')[0]; + const adventureDate = getLocalDateString(new Date(visit.start_date)); if (visit.end_date) { const endDate = new Date(visit.end_date).toISOString().split('T')[0]; // 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.setUTCDate(startDate.getUTCDate() + i); - const dateString = currentDate.toISOString().split('T')[0]; + currentDate.setDate(startDate.getDate() + i); + const dateString = getLocalDateString(currentDate); // Include the current day if it falls within the adventure date range if (dateString >= adventureDate && dateString <= endDate) { @@ -116,22 +116,22 @@ export function groupTransportationsByDate( // Initialize all days in the range for (let i = 0; i < numberOfDays; i++) { const currentDate = new Date(startDate); - currentDate.setUTCDate(startDate.getUTCDate() + i); - const dateString = currentDate.toISOString().split('T')[0]; + currentDate.setDate(startDate.getDate() + i); + const dateString = getLocalDateString(currentDate); groupedTransportations[dateString] = []; } transportations.forEach((transportation) => { if (transportation.date) { - const transportationDate = new Date(transportation.date).toISOString().split('T')[0]; + const transportationDate = getLocalDateString(new Date(transportation.date)); if (transportation.end_date) { const endDate = new Date(transportation.end_date).toISOString().split('T')[0]; // 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.setUTCDate(startDate.getUTCDate() + i); - const dateString = currentDate.toISOString().split('T')[0]; + currentDate.setDate(startDate.getDate() + i); + const dateString = getLocalDateString(currentDate); // Include the current day if it falls within the transportation date range if (dateString >= transportationDate && dateString <= endDate) { @@ -150,6 +150,13 @@ export function groupTransportationsByDate( return groupedTransportations; } +function getLocalDateString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + export function groupLodgingByDate( transportations: Lodging[], startDate: Date, @@ -157,35 +164,32 @@ export function groupLodgingByDate( ): Record<string, Lodging[]> { const groupedTransportations: Record<string, Lodging[]> = {}; - // Initialize all days in the range + // Initialize all days in the range using local dates for (let i = 0; i < numberOfDays; i++) { const currentDate = new Date(startDate); - currentDate.setUTCDate(startDate.getUTCDate() + i); - const dateString = currentDate.toISOString().split('T')[0]; + currentDate.setDate(startDate.getDate() + i); + const dateString = getLocalDateString(currentDate); groupedTransportations[dateString] = []; } transportations.forEach((transportation) => { if (transportation.check_in) { - const transportationDate = new Date(transportation.check_in).toISOString().split('T')[0]; + // Use local date string conversion + const transportationDate = getLocalDateString(new Date(transportation.check_in)); if (transportation.check_out) { - const endDate = new Date(transportation.check_out).toISOString().split('T')[0]; + const endDate = getLocalDateString(new Date(transportation.check_out)); - // Loop through all days and include transportation if it falls within the range + // 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.setUTCDate(startDate.getUTCDate() + i); - const dateString = currentDate.toISOString().split('T')[0]; + currentDate.setDate(startDate.getDate() + i); + const dateString = getLocalDateString(currentDate); - // Include the current day if it falls within the transportation date range if (dateString >= transportationDate && dateString <= endDate) { - if (groupedTransportations[dateString]) { - groupedTransportations[dateString].push(transportation); - } + groupedTransportations[dateString].push(transportation); } } } else if (groupedTransportations[transportationDate]) { - // If there's no end date, add transportation to the start date only groupedTransportations[transportationDate].push(transportation); } } @@ -201,19 +205,18 @@ export function groupNotesByDate( ): Record<string, Note[]> { const groupedNotes: Record<string, Note[]> = {}; - // Initialize all days in the range + // Initialize all days in the range using local dates for (let i = 0; i < numberOfDays; i++) { const currentDate = new Date(startDate); - currentDate.setUTCDate(startDate.getUTCDate() + i); - const dateString = currentDate.toISOString().split('T')[0]; + currentDate.setDate(startDate.getDate() + i); + const dateString = getLocalDateString(currentDate); groupedNotes[dateString] = []; } notes.forEach((note) => { if (note.date) { - const noteDate = new Date(note.date).toISOString().split('T')[0]; - - // Add note to the appropriate date group if it exists + // Use the date string as is since it's already in "YYYY-MM-DD" format. + const noteDate = note.date; if (groupedNotes[noteDate]) { groupedNotes[noteDate].push(note); } @@ -230,19 +233,18 @@ export function groupChecklistsByDate( ): Record<string, Checklist[]> { const groupedChecklists: Record<string, Checklist[]> = {}; - // Initialize all days in the range + // Initialize all days in the range using local dates for (let i = 0; i < numberOfDays; i++) { const currentDate = new Date(startDate); - currentDate.setUTCDate(startDate.getUTCDate() + i); - const dateString = currentDate.toISOString().split('T')[0]; + currentDate.setDate(startDate.getDate() + i); + const dateString = getLocalDateString(currentDate); groupedChecklists[dateString] = []; } checklists.forEach((checklist) => { if (checklist.date) { - const checklistDate = new Date(checklist.date).toISOString().split('T')[0]; - - // Add checklist to the appropriate date group if it exists + // Use the date string as is since it's already in "YYYY-MM-DD" format. + const checklistDate = checklist.date; if (groupedChecklists[checklistDate]) { groupedChecklists[checklistDate].push(checklist); } diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index b6ffac3a..1a69fdd0 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -935,24 +935,25 @@ {@const dateString = adjustedDate.toISOString().split('T')[0]} {@const dayAdventures = - groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays)[ + groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays + 1)[ dateString ] || []} {@const dayTransportations = groupTransportationsByDate( transportations, new Date(collection.start_date), - numberOfDays + 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 = - groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] || - []} + groupNotesByDate(notes, new Date(collection.start_date), numberOfDays + 1)[ + dateString + ] || []} {@const dayChecklists = - groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays)[ + groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays + 1)[ dateString ] || []} From 771579ef3d83d158f6acaf53b2b6ccc7b8a5e996 Mon Sep 17 00:00:00 2001 From: Sean Morley <zipsm15@gmail.com> Date: Thu, 20 Mar 2025 10:26:41 -0400 Subject: [PATCH 05/10] feat: Improve adventure date grouping to handle all-day events and enhance date formatting --- frontend/src/lib/index.ts | 80 +++++++++++++++------- frontend/src/routes/signup/+page.server.ts | 2 - 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index af68de26..d66abec7 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -78,26 +78,57 @@ export function groupAdventuresByDate( adventures.forEach((adventure) => { adventure.visits.forEach((visit) => { if (visit.start_date) { - const adventureDate = getLocalDateString(new Date(visit.start_date)); - if (visit.end_date) { - const endDate = new Date(visit.end_date).toISOString().split('T')[0]; + // 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); - // Loop through all days and include adventure if it falls within the range + // 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]; + + // 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 dateString = getLocalDateString(currentDate); + const currentDateStr = getLocalDateString(currentDate); // Include the current day if it falls within the adventure date range - if (dateString >= adventureDate && dateString <= endDate) { - if (groupedAdventures[dateString]) { - groupedAdventures[dateString].push(adventure); + if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) { + if (groupedAdventures[currentDateStr]) { + groupedAdventures[currentDateStr].push(adventure); } } } - } else if (groupedAdventures[adventureDate]) { - // If there's no end date, add adventure to the start date only - groupedAdventures[adventureDate].push(adventure); + } else { + // Handle regular events with time components + const adventureStartDate = new Date(visit.start_date); + const adventureDateStr = getLocalDateString(adventureStartDate); + + if (visit.end_date) { + const adventureEndDate = new Date(visit.end_date); + const endDateStr = getLocalDateString(adventureEndDate); + + // 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); + + // 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); + } + } } } }); @@ -106,6 +137,20 @@ export function groupAdventuresByDate( return groupedAdventures; } +function getLocalDateString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed + const day = String(date.getDate()).padStart(2, '0'); + 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, @@ -150,13 +195,6 @@ export function groupTransportationsByDate( return groupedTransportations; } -function getLocalDateString(date: Date): string { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; -} - export function groupLodgingByDate( transportations: Lodging[], startDate: Date, @@ -351,12 +389,6 @@ export let TRANSPORTATION_TYPES_ICONS = { other: '❓' }; -// Helper to check if a given date string represents midnight (all-day) -export function isAllDay(dateStr: string | string[]) { - // Checks for the pattern "T00:00:00.000Z" - return dateStr.includes('T00:00:00Z') || dateStr.includes('T00:00:00.000Z'); -} - export function getAdventureTypeLabel(type: string) { // return the emoji ADVENTURE_TYPE_ICONS label for the given type if not found return ? emoji if (type in ADVENTURE_TYPE_ICONS) { diff --git a/frontend/src/routes/signup/+page.server.ts b/frontend/src/routes/signup/+page.server.ts index 1e39414f..61f08525 100644 --- a/frontend/src/routes/signup/+page.server.ts +++ b/frontend/src/routes/signup/+page.server.ts @@ -74,8 +74,6 @@ export const actions: Actions = { } else { const setCookieHeader = loginFetch.headers.get('Set-Cookie'); - console.log('setCookieHeader:', setCookieHeader); - if (setCookieHeader) { // Regular expression to match sessionid cookie and its expiry const sessionIdRegex = /sessionid=([^;]+).*?expires=([^;]+)/; From 1042a3edcc93ff184d11b4b9addff7c54f9b3756 Mon Sep 17 00:00:00 2001 From: Sean Morley <zipsm15@gmail.com> Date: Thu, 20 Mar 2025 22:08:22 -0400 Subject: [PATCH 06/10] refactor: Remove debug print statement from NoPasswordAuthBackend authentication method --- backend/server/users/backends.py | 1 - .../src/routes/collections/[id]/+page.svelte | 376 +++++++++++------- 2 files changed, 224 insertions(+), 153 deletions(-) diff --git a/backend/server/users/backends.py b/backend/server/users/backends.py index a099f118..f9291f0a 100644 --- a/backend/server/users/backends.py +++ b/backend/server/users/backends.py @@ -3,7 +3,6 @@ from allauth.socialaccount.models import SocialAccount class NoPasswordAuthBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): - print("NoPasswordAuthBackend") # First, attempt normal authentication user = super().authenticate(request, username=username, password=password, **kwargs) if user is None: diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 1a69fdd0..df8ad0a2 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -133,6 +133,7 @@ } let currentView: string = 'itinerary'; + let currentItineraryView: string = 'date'; let adventures: Adventure[] = []; @@ -303,6 +304,7 @@ } else { notFound = true; } + if (collection.start_date && collection.end_date) { numberOfDays = Math.floor( @@ -923,129 +925,233 @@ })}</span > </p> + <div class="join mt-2"> + <input + class="join-item btn btn-neutral" + type="radio" + name="options" + aria-label="Date Itinerary" + checked={currentItineraryView == 'date'} + on:change={() => (currentItineraryView = 'date')} + /> + <input + class="join-item btn btn-neutral" + type="radio" + name="options" + aria-label="Ordered Itinerary" + checked={currentItineraryView == 'ordered'} + on:change={() => (currentItineraryView = 'ordered')} + /> + </div> </div> </div> </div> - <div class="container mx-auto px-4"> - {#each Array(numberOfDays) as _, i} - {@const startDate = new Date(collection.start_date)} - {@const tempDate = new Date(startDate.getTime())} - {@const adjustedDate = new Date(tempDate.setUTCDate(tempDate.getUTCDate() + i))} - {@const dateString = adjustedDate.toISOString().split('T')[0]} + {#if currentItineraryView == 'date'} + <div class="container mx-auto px-4"> + {#each Array(numberOfDays) as _, i} + {@const startDate = new Date(collection.start_date)} + {@const tempDate = new Date(startDate.getTime())} + {@const adjustedDate = new Date(tempDate.setUTCDate(tempDate.getUTCDate() + i))} + {@const dateString = adjustedDate.toISOString().split('T')[0]} - {@const dayAdventures = - groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays + 1)[ - dateString - ] || []} - {@const dayTransportations = - groupTransportationsByDate( - transportations, - new Date(collection.start_date), - numberOfDays + 1 - )[dateString] || []} - {@const dayLodging = - groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays + 1)[ - dateString - ] || []} - {@const dayNotes = - groupNotesByDate(notes, new Date(collection.start_date), numberOfDays + 1)[ - dateString - ] || []} - {@const dayChecklists = - groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays + 1)[ - dateString - ] || []} + {@const dayAdventures = + groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays + 1)[ + dateString + ] || []} + {@const dayTransportations = + groupTransportationsByDate( + transportations, + new Date(collection.start_date), + numberOfDays + 1 + )[dateString] || []} + {@const dayLodging = + groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays + 1)[ + dateString + ] || []} + {@const dayNotes = + groupNotesByDate(notes, new Date(collection.start_date), numberOfDays + 1)[ + dateString + ] || []} + {@const dayChecklists = + groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays + 1)[ + dateString + ] || []} - <div class="card bg-base-100 shadow-xl my-8"> - <div class="card-body bg-base-200"> - <h2 class="card-title text-3xl justify-center g"> - {$t('adventures.day')} - {i + 1} - <div class="badge badge-lg"> - {adjustedDate.toLocaleDateString(undefined, { timeZone: 'UTC' })} + <div class="card bg-base-100 shadow-xl my-8"> + <div class="card-body bg-base-200"> + <h2 class="card-title text-3xl justify-center g"> + {$t('adventures.day')} + {i + 1} + <div class="badge badge-lg"> + {adjustedDate.toLocaleDateString(undefined, { timeZone: 'UTC' })} + </div> + </h2> + + <div class="divider"></div> + + <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> + {#if dayAdventures.length > 0} + {#each dayAdventures as adventure} + <AdventureCard + user={data.user} + on:edit={editAdventure} + on:delete={deleteAdventure} + {adventure} + /> + {/each} + {/if} + {#if dayTransportations.length > 0} + {#each dayTransportations as transportation} + <TransportationCard + {transportation} + user={data?.user} + on:delete={(event) => { + transportations = transportations.filter((t) => t.id != event.detail); + }} + on:edit={(event) => { + transportationToEdit = event.detail; + isShowingTransportationModal = true; + }} + /> + {/each} + {/if} + {#if dayNotes.length > 0} + {#each dayNotes as note} + <NoteCard + {note} + user={data.user || null} + on:edit={(event) => { + noteToEdit = event.detail; + isNoteModalOpen = true; + }} + on:delete={(event) => { + notes = notes.filter((n) => n.id != event.detail); + }} + /> + {/each} + {/if} + {#if dayLodging.length > 0} + {#each dayLodging as hotel} + <LodgingCard + lodging={hotel} + user={data?.user} + on:delete={(event) => { + lodging = lodging.filter((t) => t.id != event.detail); + }} + on:edit={editLodging} + /> + {/each} + {/if} + {#if dayChecklists.length > 0} + {#each dayChecklists as checklist} + <ChecklistCard + {checklist} + user={data.user || null} + on:delete={(event) => { + notes = notes.filter((n) => n.id != event.detail); + }} + on:edit={(event) => { + checklistToEdit = event.detail; + isShowingChecklistModal = true; + }} + /> + {/each} + {/if} </div> - </h2> - <div class="divider"></div> - - <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> - {#if dayAdventures.length > 0} - {#each dayAdventures as adventure} - <AdventureCard - user={data.user} - on:edit={editAdventure} - on:delete={deleteAdventure} - {adventure} - /> - {/each} - {/if} - {#if dayTransportations.length > 0} - {#each dayTransportations as transportation} - <TransportationCard - {transportation} - user={data?.user} - on:delete={(event) => { - transportations = transportations.filter((t) => t.id != event.detail); - }} - on:edit={(event) => { - transportationToEdit = event.detail; - isShowingTransportationModal = true; - }} - /> - {/each} - {/if} - {#if dayNotes.length > 0} - {#each dayNotes as note} - <NoteCard - {note} - user={data.user || null} - on:edit={(event) => { - noteToEdit = event.detail; - isNoteModalOpen = true; - }} - on:delete={(event) => { - notes = notes.filter((n) => n.id != event.detail); - }} - /> - {/each} - {/if} - {#if dayLodging.length > 0} - {#each dayLodging as hotel} - <LodgingCard - lodging={hotel} - user={data?.user} - on:delete={(event) => { - lodging = lodging.filter((t) => t.id != event.detail); - }} - on:edit={editLodging} - /> - {/each} - {/if} - {#if dayChecklists.length > 0} - {#each dayChecklists as checklist} - <ChecklistCard - {checklist} - user={data.user || null} - on:delete={(event) => { - notes = notes.filter((n) => n.id != event.detail); - }} - on:edit={(event) => { - checklistToEdit = event.detail; - isShowingChecklistModal = true; - }} - /> - {/each} + {#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0 && dayLodging.length == 0} + <p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p> {/if} </div> - - {#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0 && dayLodging.length == 0} - <p class="text-center text-lg mt-2 italic">{$t('adventures.nothing_planned')}</p> + </div> + {/each} + </div> + {:else} + <div class="container mx-auto px-4 py-8"> + <div class="flex flex-col items-center"> + <div class="w-full max-w-4xl relative"> + <!-- Vertical timeline line that spans the entire height --> + <div class="absolute left-8 top-0 bottom-0 w-1 bg-primary"></div> + <ul class="relative"> + {#each orderedItems as orderedItem, index} + <li class="relative pl-20 mb-8"> + <!-- Timeline Icon --> + <div + class="absolute left-0 top-0 flex items-center justify-center w-16 h-16 bg-base-200 rounded-full border-2 border-primary" + > + {#if orderedItem.type === 'adventure' && orderedItem.item && 'category' in orderedItem.item && orderedItem.item.category && 'icon' in orderedItem.item.category} + <span class="text-2xl">{orderedItem.item.category.icon}</span> + {:else if orderedItem.type === 'transportation' && orderedItem.item && 'origin_latitude' in orderedItem.item} + <span class="text-2xl">{getTransportationEmoji(orderedItem.item.type)}</span + > + {:else if orderedItem.type === 'lodging' && orderedItem.item && 'reservation_number' in orderedItem.item} + <span class="text-2xl">{getLodgingIcon(orderedItem.item.type)}</span> + {/if} + </div> + <!-- Card Content --> + <div class="bg-base-200 p-6 rounded-lg shadow-lg"> + <div class="flex justify-between items-center mb-4"> + <span class="badge badge-lg">{orderedItem.type}</span> + <div class="text-sm opacity-80 text-right"> + {new Date(orderedItem.start).toLocaleDateString(undefined, { + month: 'short', + day: 'numeric' + })} + {#if orderedItem.start !== orderedItem.end} + <div> + {new Date(orderedItem.start).toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit' + })} + </div> + {:else} + <p>{$t('adventures.all_day')} ⏱️</p> + {/if} + </div> + </div> + {#if orderedItem.type === 'adventure' && orderedItem.item && 'images' in orderedItem.item} + <AdventureCard + user={data.user} + on:edit={editAdventure} + on:delete={deleteAdventure} + adventure={orderedItem.item} + {collection} + /> + {:else if orderedItem.type === 'transportation' && orderedItem.item && 'origin_latitude' in orderedItem.item} + <TransportationCard + transportation={orderedItem.item} + user={data?.user} + on:delete={(event) => { + transportations = transportations.filter((t) => t.id != event.detail); + }} + on:edit={editTransportation} + {collection} + /> + {:else if orderedItem.type === 'lodging' && orderedItem.item && 'reservation_number' in orderedItem.item} + <LodgingCard + lodging={orderedItem.item} + user={data?.user} + on:delete={(event) => { + lodging = lodging.filter((t) => t.id != event.detail); + }} + on:edit={editLodging} + {collection} + /> + {/if} + </div> + </li> + {/each} + </ul> + {#if orderedItems.length === 0} + <div class="alert alert-info"> + <p class="text-center text-lg">{$t('adventures.nothing_planned')}</p> + </div> {/if} </div> </div> - {/each} - </div> + </div> + {/if} {/if} {/if} @@ -1331,13 +1437,16 @@ {recomendation.name || $t('recomendations.recommendation')} </h2> <div class="badge badge-primary">{recomendation.tag}</div> - {#if recomendation.address} + {#if recomendation.address && (recomendation.address.housenumber || recomendation.address.street || recomendation.address.city || recomendation.address.state || recomendation.address.postcode)} <p class="text-md"> <strong>{$t('recomendations.address')}:</strong> - {recomendation.address.housenumber} - {recomendation.address.street}, {recomendation.address.city}, {recomendation - .address.state} - {recomendation.address.postcode} + {#if recomendation.address.housenumber}{recomendation.address + .housenumber}{/if} + {#if recomendation.address.street} + {recomendation.address.street}{/if} + {#if recomendation.address.city}, {recomendation.address.city}{/if} + {#if recomendation.address.state}, {recomendation.address.state}{/if} + {#if recomendation.address.postcode}, {recomendation.address.postcode}{/if} </p> {/if} {#if recomendation.contact} @@ -1389,43 +1498,6 @@ {/if} {/if} -{#each orderedItems as orderedItem} - <p>{orderedItem.type}</p> - {#if orderedItem.type === 'adventure'} - {#if orderedItem.item && 'images' in orderedItem.item} - <AdventureCard - user={data.user} - on:edit={editAdventure} - on:delete={deleteAdventure} - adventure={orderedItem.item} - {collection} - /> - {/if} - {/if} - {#if orderedItem.type === 'transportation' && orderedItem.item && 'origin_latitude' in orderedItem.item} - <TransportationCard - transportation={orderedItem.item} - user={data?.user} - on:delete={(event) => { - transportations = transportations.filter((t) => t.id != event.detail); - }} - on:edit={editTransportation} - {collection} - /> - {/if} - {#if orderedItem.type === 'lodging' && orderedItem.item && 'reservation_number' in orderedItem.item} - <LodgingCard - lodging={orderedItem.item} - user={data?.user} - on:delete={(event) => { - lodging = lodging.filter((t) => t.id != event.detail); - }} - on:edit={editLodging} - {collection} - /> - {/if} -{/each} - <svelte:head> <title >{data.props.adventure && data.props.adventure.name From f79b06f6b3f35a4e8ceb1d77edcdae536be2e44c Mon Sep 17 00:00:00 2001 From: Sean Morley <zipsm15@gmail.com> Date: Thu, 20 Mar 2025 22:28:23 -0400 Subject: [PATCH 07/10] feat: Add troubleshooting guide for unresponsive login and registration, enhance collection modal alerts, and improve localization for itinerary features --- documentation/.vitepress/config.mts | 4 +++ .../troubleshooting/login_unresponsive.md | 19 ++++++++++++++ .../src/lib/components/CollectionModal.svelte | 25 +++++++++++++++++-- frontend/src/locales/en.json | 4 +++ .../src/routes/collections/[id]/+page.svelte | 10 +++++--- frontend/src/routes/worldtravel/+page.svelte | 8 ++++++ 6 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 documentation/docs/troubleshooting/login_unresponsive.md diff --git a/documentation/.vitepress/config.mts b/documentation/.vitepress/config.mts index dbd92993..4263aca9 100644 --- a/documentation/.vitepress/config.mts +++ b/documentation/.vitepress/config.mts @@ -134,6 +134,10 @@ export default defineConfig({ text: "No Images Displaying", link: "/docs/troubleshooting/no_images", }, + { + text: "Login and Registration Unresponsive", + link: "/docs/troubleshooting/login_unresponsive", + }, { text: "Failed to Start Nginx", link: "/docs/troubleshooting/nginx_failed", diff --git a/documentation/docs/troubleshooting/login_unresponsive.md b/documentation/docs/troubleshooting/login_unresponsive.md new file mode 100644 index 00000000..aecf4311 --- /dev/null +++ b/documentation/docs/troubleshooting/login_unresponsive.md @@ -0,0 +1,19 @@ +# Troubleshooting: Login and Registration Unresponsive + +When you encounter issues with the login and registration pages being unresponsive in AdventureLog, it can be due to various reasons. This guide will help you troubleshoot and resolve the unresponsive login and registration pages in AdventureLog. + +1. Check to make sure the backend container is running and accessible. + + - Check the backend container logs to see if there are any errors or issues blocking the contianer from running. + +2. Check the connection between the frontend and backend containers. + + - Attempt login with the browser console network tab open to see if there are any errors or issues with the connection between the frontend and backend containers. If there is a connection issue, the code will show an error like `Failed to load resource: net::ERR_CONNECTION_REFUSED`. If this is the case, check the `PUBLIC_SERVER_URL` in the frontend container and refer to the installation docs to ensure the correct URL is set. + - If the error is `403`, continue to the next step. + +3. The error most likely is due to a CSRF security config issue in either the backend or frontend. + + - Check that the `ORIGIN` variable in the frontend is set to the URL where the frontend is access and you are accessing the app from currently. + - Check that the `CSRF_TRUSTED_ORIGINS` variable in the backend is set to a comma separated list of the origins where you use your backend server and frontend. One of these values should match the `ORIGIN` variable in the frontend. + +4. If you are still experiencing issues, please refer to the [AdventureLog Discord Server](https://discord.gg/wRbQ9Egr8C) for further assistance, providing as much detail as possible about the issue you are experiencing! diff --git a/frontend/src/lib/components/CollectionModal.svelte b/frontend/src/lib/components/CollectionModal.svelte index e5cb294f..93ec776b 100644 --- a/frontend/src/lib/components/CollectionModal.svelte +++ b/frontend/src/lib/components/CollectionModal.svelte @@ -189,10 +189,31 @@ </div> </div> </div> - <!-- Form Actions --> + + {#if !collection.start_date && !collection.end_date} + <div class="mt-4"> + <div role="alert" class="alert alert-neutral"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + class="h-6 w-6 shrink-0 stroke-current" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" + ></path> + </svg> + <span>{$t('adventures.collection_no_start_end_date')}</span> + </div> + </div> + {/if} + <div class="mt-4"> <button type="submit" class="btn btn-primary"> - {$t('adventures.save_next')} + {$t('notes.save')} </button> <button type="button" class="btn" on:click={close}> {$t('about.close')} diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 4e32577b..72d18833 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -131,6 +131,7 @@ "search_for_location": "Search for a location", "clear_map": "Clear map", "search_results": "Searh results", + "collection_no_start_end_date": "Adding a start and end date to the collection will unlock itinerary planning features in the collection page.", "no_results": "No results found", "wiki_desc": "Pulls excerpt from Wikipedia article matching the name of the adventure.", "attachments": "Attachments", @@ -251,6 +252,9 @@ "emoji_picker": "Emoji Picker", "download_calendar": "Download Calendar", "all_day": "All Day", + "ordered_itinerary": "Ordered Itinerary", + "date_itinerary": "Date Itinerary", + "no_ordered_items": "Add items with dates to the collection to see them here.", "date_information": "Date Information", "flight_information": "Flight Information", "out_of_range": "Not in itinerary date range", diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index df8ad0a2..4bdba311 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -930,7 +930,7 @@ class="join-item btn btn-neutral" type="radio" name="options" - aria-label="Date Itinerary" + aria-label={$t('adventures.date_itinerary')} checked={currentItineraryView == 'date'} on:change={() => (currentItineraryView = 'date')} /> @@ -938,7 +938,7 @@ class="join-item btn btn-neutral" type="radio" name="options" - aria-label="Ordered Itinerary" + aria-label={$t('adventures.ordered_itinerary')} checked={currentItineraryView == 'ordered'} on:change={() => (currentItineraryView = 'ordered')} /> @@ -1072,7 +1072,9 @@ <div class="flex flex-col items-center"> <div class="w-full max-w-4xl relative"> <!-- Vertical timeline line that spans the entire height --> - <div class="absolute left-8 top-0 bottom-0 w-1 bg-primary"></div> + {#if orderedItems.length > 0} + <div class="absolute left-8 top-0 bottom-0 w-1 bg-primary"></div> + {/if} <ul class="relative"> {#each orderedItems as orderedItem, index} <li class="relative pl-20 mb-8"> @@ -1145,7 +1147,7 @@ </ul> {#if orderedItems.length === 0} <div class="alert alert-info"> - <p class="text-center text-lg">{$t('adventures.nothing_planned')}</p> + <p class="text-center text-lg">{$t('adventures.no_ordered_items')}</p> </div> {/if} </div> diff --git a/frontend/src/routes/worldtravel/+page.svelte b/frontend/src/routes/worldtravel/+page.svelte index dad43a1a..aeabc14b 100644 --- a/frontend/src/routes/worldtravel/+page.svelte +++ b/frontend/src/routes/worldtravel/+page.svelte @@ -164,6 +164,14 @@ {#if filteredCountries.length === 0} <p class="text-center font-bold text-2xl mt-12">{$t('worldtravel.no_countries_found')}</p> + + <div class="text-center mt-4"> + <a + class="link link-primary" + href="https://adventurelog.app/docs/configuration/updating.html#updating-the-region-data" + target="_blank">{$t('settings.documentation_link')}</a + > + </div> {/if} <svelte:head> From db63b6e7d8b65f22b89ac021b6ad708eb6373d5c Mon Sep 17 00:00:00 2001 From: Sean Morley <zipsm15@gmail.com> Date: Fri, 21 Mar 2025 13:35:29 -0400 Subject: [PATCH 08/10] feat: Add usage guide for AdventureLog and enhance overview with personal message from the maintainer --- documentation/.vitepress/config.mts | 10 +++++++ .../docs/intro/adventurelog_overview.md | 4 ++- documentation/docs/usage/usage.md | 29 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 documentation/docs/usage/usage.md diff --git a/documentation/.vitepress/config.mts b/documentation/.vitepress/config.mts index 4263aca9..4877f628 100644 --- a/documentation/.vitepress/config.mts +++ b/documentation/.vitepress/config.mts @@ -84,6 +84,16 @@ export default defineConfig({ }, ], }, + { + text: "Usage", + collapsed: false, + items: [ + { + text: "How to use AdventureLog", + link: "/docs/usage/usage", + }, + ], + }, { text: "Configuration", collapsed: false, diff --git a/documentation/docs/intro/adventurelog_overview.md b/documentation/docs/intro/adventurelog_overview.md index 310237ff..cfb30f4c 100644 --- a/documentation/docs/intro/adventurelog_overview.md +++ b/documentation/docs/intro/adventurelog_overview.md @@ -27,4 +27,6 @@ AdventureLog is open-source software, licensed under the GPL-3.0 license. This m ## About the Maintainer -AdventureLog is created and maintained by [Sean Morley](https://seanmorley.com), a Computer Science student at the University of Connecticut. Sean is passionate about open-source software and building modern tools that help people solve real-world problems. +Hi, I'm [Sean Morley](https://seanmorley.com), the creator of AdventureLog. I'm a Computer Science student at the University of Connecticut, and I'm passionate about open-source software and building modern tools that help people solve real-world problems. I created AdventureLog to solve a problem: the lack of a modern, open-source, user-friendly travel companion. Many existing travel apps are either too complex, too expensive, or too closed-off to be useful for the average traveler. AdventureLog aims to be the opposite: simple, beautiful, and open to everyone. + +I hope you enjoy using AdventureLog as much as I enjoy creating it! If you have any questions, feedback, or suggestions, feel free to reach out to me via the email address listed on my website. I'm always happy to hear from users and help in any way I can. Thank you for using AdventureLog, and happy travels! 🌍 diff --git a/documentation/docs/usage/usage.md b/documentation/docs/usage/usage.md new file mode 100644 index 00000000..8cf01299 --- /dev/null +++ b/documentation/docs/usage/usage.md @@ -0,0 +1,29 @@ +# How to use AdventureLog + +Welcome to AdventureLog! This guide will help you get started with AdventureLog and provide you with an overview of the features available to you. + +## Key Terms + +#### Adventures + +- **Adventure**: think of an adventure as a point on a map, a location you want to visit, or a place you want to explore. An adventure can be anything you want it to be, from a local park to a famous landmark. +- **Visit**: a visit is added to an adventure. It contains a date and notes about when the adventure was visited. If an adventure is visited multiple times, multiple visits can be added. If there are no visits on an adventure or the date of all visits is in the future, the adventure is considered planned. If the date of the visit is in the past, the adventure is considered completed. +- **Category**: a category is a way to group adventures together. For example, you could have a category for parks, a category for museums, and a category for restaurants. +- **Tag**: a tag is a way to add additional information to an adventure. For example, you could have a tag for the type of cuisine at a restaurant or the type of art at a museum. Multiple tags can be added to an adventure. +- **Image**: an image is a photo that is added to an adventure. Images can be added to an adventure to provide a visual representation of the location or to capture a memory of the visit. These can be uploded from your device or with a service like [Immich](/docs/configuration/immich_integration) if the integration is enabled. +- **Attachment**: an attachment is a file that is added to an adventure. Attachments can be added to an adventure to provide additional information, such as a map of the location or a brochure from the visit. + +#### Collections + +- **Collection**: a collection is a way to group adventures together. Collections are flexible and can be used in many ways. When no start or end date is added to a collection, it acts like a folder to group adventures together. When a start and end date is added to a collection, it acts like a trip to group adventures together that were visited during that time period. With start and end dates, the collection is transformed into a full itinerary with a map showing the route taken between adventures. +- **Transportation**: a transportation is a collection exclusive feature that allows you to add transportation information to your trip. This can be used to show the route taken between locations and the mode of transportation used. It can also be used to track flight information, such as flight number and departure time. +- **Lodging**: a lodging is a collection exclusive feature that allows you to add lodging information to your trip. This can be used to plan where you will stay during your trip and add notes about the lodging location. It can also be used to track reservation information, such as reservation number and check-in time. +- **Note**: a note is a collection exclusive feature that allows you to add notes to your trip. This can be used to add additional information about your trip, such as a summary of the trip or a list of things to do. Notes can be assigned to a specific day of the trip to help organize the information. +- **Checklist**: a checklist is a collection exclusive feature that allows you to add a checklist to your trip. This can be used to create a list of things to do during your trip or for planning purposes like packing lists. Checklists can be assigned to a specific day of the trip to help organize the information. + +#### World Travel + +- **World Travel**: the world travel feature of AdventureLog allows you to track the countries, regions, and cities you have visited during your lifetime. You can add visits to countries, regions, and cities, and view statistics about your travels. The world travel feature is a fun way to visualize where you have been and where you want to go next. + - **Country**: a country is a geographical area that is recognized as an independent nation. You can add visits to countries to track where you have been. + - **Region**: a region is a geographical area that is part of a country. You can add visits to regions to track where you have been within a country. + - **City**: a city is a geographical area that is a populated urban center. You can add visits to cities to track where you have been within a region. From 794df82ec62f7b99ab0262e880bdfeb1a1cd80a8 Mon Sep 17 00:00:00 2001 From: Sean Morley <zipsm15@gmail.com> Date: Fri, 21 Mar 2025 16:30:03 -0400 Subject: [PATCH 09/10] feat: Enhance AdventureModal date handling for all-day events and improve localization in collections page --- .../src/lib/components/AdventureModal.svelte | 59 +++++++++++++++++-- .../src/routes/collections/[id]/+page.svelte | 32 +++++++++- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 3054687d..85db0a4b 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -8,12 +8,16 @@ let fullStartDate: string = ''; let fullEndDate: string = ''; + let fullStartDateOnly: string = ''; + let fullEndDateOnly: string = ''; let allDay: boolean = true; // Set full start and end dates from collection if (collection && collection.start_date && collection.end_date) { fullStartDate = `${collection.start_date}T00:00`; fullEndDate = `${collection.end_date}T23:59`; + fullStartDateOnly = collection.start_date; + fullEndDateOnly = collection.end_date; } const dispatch = createEventDispatcher(); @@ -742,8 +746,8 @@ type="date" class="input input-bordered w-full" placeholder={$t('adventures.start_date')} - min={constrainDates ? fullStartDate : ''} - max={constrainDates ? fullEndDate : ''} + min={constrainDates ? fullStartDateOnly : ''} + max={constrainDates ? fullEndDateOnly : ''} bind:value={new_start_date} on:keydown={(e) => { if (e.key === 'Enter') { @@ -757,8 +761,8 @@ class="input input-bordered w-full" placeholder={$t('adventures.end_date')} bind:value={new_end_date} - min={constrainDates ? fullStartDate : ''} - max={constrainDates ? fullEndDate : ''} + min={constrainDates ? fullStartDateOnly : ''} + max={constrainDates ? fullEndDateOnly : ''} on:keydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -848,6 +852,53 @@ </p> {/if} <div> + <button + type="button" + class="btn btn-sm btn-neutral" + on:click={() => { + // Determine if this is an all-day event + const isAllDayEvent = isAllDay(visit.start_date); + allDay = isAllDayEvent; + + if (isAllDayEvent) { + // For all-day events, use date only + new_start_date = visit.start_date.split('T')[0]; + new_end_date = visit.end_date.split('T')[0]; + } else { + // For timed events, format properly for datetime-local input + const startDate = new Date(visit.start_date); + const endDate = new Date(visit.end_date); + + // Format as yyyy-MM-ddThh:mm + new_start_date = + startDate.getFullYear() + + '-' + + String(startDate.getMonth() + 1).padStart(2, '0') + + '-' + + String(startDate.getDate()).padStart(2, '0') + + 'T' + + String(startDate.getHours()).padStart(2, '0') + + ':' + + String(startDate.getMinutes()).padStart(2, '0'); + + new_end_date = + endDate.getFullYear() + + '-' + + String(endDate.getMonth() + 1).padStart(2, '0') + + '-' + + String(endDate.getDate()).padStart(2, '0') + + 'T' + + String(endDate.getHours()).padStart(2, '0') + + ':' + + String(endDate.getMinutes()).padStart(2, '0'); + } + + new_notes = visit.notes; + adventure.visits = adventure.visits.filter((v) => v !== visit); + }} + > + {$t('lodging.edit')} + </button> <button type="button" class="btn btn-sm btn-error" diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 4bdba311..2578384e 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -1094,7 +1094,7 @@ <!-- Card Content --> <div class="bg-base-200 p-6 rounded-lg shadow-lg"> <div class="flex justify-between items-center mb-4"> - <span class="badge badge-lg">{orderedItem.type}</span> + <span class="badge badge-lg">{$t(`adventures.${orderedItem.type}`)}</span> <div class="text-sm opacity-80 text-right"> {new Date(orderedItem.start).toLocaleDateString(undefined, { month: 'short', @@ -1106,6 +1106,36 @@ hour: '2-digit', minute: '2-digit' })} + - + {new Date(orderedItem.end).toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit' + })} + </div> + <div> + <!-- Duration --> + {Math.round( + (new Date(orderedItem.end).getTime() - + new Date(orderedItem.start).getTime()) / + 1000 / + 60 / + 60 + )}h + {Math.round( + ((new Date(orderedItem.end).getTime() - + new Date(orderedItem.start).getTime()) / + 1000 / + 60 / + 60 - + Math.floor( + (new Date(orderedItem.end).getTime() - + new Date(orderedItem.start).getTime()) / + 1000 / + 60 / + 60 + )) * + 60 + )}m </div> {:else} <p>{$t('adventures.all_day')} ⏱️</p> From fe25f8e2c8574fed89a009d631b8d5b2d0b9b932 Mon Sep 17 00:00:00 2001 From: Sean Morley <zipsm15@gmail.com> Date: Fri, 21 Mar 2025 17:31:33 -0400 Subject: [PATCH 10/10] feat: Add start_date to collection ordering and enhance localization for itinerary features --- .../adventures/views/collection_view.py | 8 +++- frontend/src/locales/de.json | 6 ++- frontend/src/locales/es.json | 6 ++- frontend/src/locales/fr.json | 6 ++- frontend/src/locales/it.json | 6 ++- frontend/src/locales/ko.json | 6 ++- frontend/src/locales/nl.json | 6 ++- frontend/src/locales/pl.json | 6 ++- frontend/src/locales/sv.json | 6 ++- frontend/src/locales/zh.json | 6 ++- .../src/routes/collections/+page.server.ts | 4 +- frontend/src/routes/collections/+page.svelte | 39 +++++++++++++------ 12 files changed, 82 insertions(+), 23 deletions(-) diff --git a/backend/server/adventures/views/collection_view.py b/backend/server/adventures/views/collection_view.py index f0529ee6..2c46dc5d 100644 --- a/backend/server/adventures/views/collection_view.py +++ b/backend/server/adventures/views/collection_view.py @@ -22,7 +22,7 @@ class CollectionViewSet(viewsets.ModelViewSet): order_by = self.request.query_params.get('order_by', 'name') order_direction = self.request.query_params.get('order_direction', 'asc') - valid_order_by = ['name', 'upated_at'] + valid_order_by = ['name', 'upated_at', 'start_date'] if order_by not in valid_order_by: order_by = 'updated_at' @@ -35,6 +35,12 @@ class CollectionViewSet(viewsets.ModelViewSet): ordering = 'lower_name' if order_direction == 'desc': ordering = f'-{ordering}' + elif order_by == 'start_date': + ordering = 'start_date' + if order_direction == 'asc': + ordering = 'start_date' + else: + ordering = '-start_date' else: order_by == 'updated_at' ordering = 'updated_at' diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index c0d939eb..13a02ad4 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -248,7 +248,11 @@ "reservation_number": "Reservierungsnummer", "welcome_map_info": "Frei zugängliche Abenteuer auf diesem Server", "open_in_maps": "In Karten öffnen", - "all_day": "Den ganzen Tag" + "all_day": "Den ganzen Tag", + "collection_no_start_end_date": "Durch das Hinzufügen eines Start- und Enddatums zur Sammlung werden Reiseroutenplanungsfunktionen auf der Sammlungsseite freigegeben.", + "date_itinerary": "Datumstrecke", + "no_ordered_items": "Fügen Sie der Sammlung Elemente mit Daten hinzu, um sie hier zu sehen.", + "ordered_itinerary": "Reiseroute bestellt" }, "home": { "desc_1": "Entdecken, planen und erkunden Sie mühelos", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index bd9b6a4e..c82df3f2 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -296,7 +296,11 @@ "reservation_number": "Número de reserva", "welcome_map_info": "Aventuras públicas en este servidor", "open_in_maps": "Abrir en mapas", - "all_day": "Todo el día" + "all_day": "Todo el día", + "collection_no_start_end_date": "Agregar una fecha de inicio y finalización a la colección desbloqueará las funciones de planificación del itinerario en la página de colección.", + "date_itinerary": "Itinerario de fecha", + "no_ordered_items": "Agregue elementos con fechas a la colección para verlos aquí.", + "ordered_itinerary": "Itinerario ordenado" }, "worldtravel": { "all": "Todo", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index 2523fe5e..249243d8 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -248,7 +248,11 @@ "reservation_number": "Numéro de réservation", "welcome_map_info": "Aventures publiques sur ce serveur", "open_in_maps": "Ouvert dans les cartes", - "all_day": "Toute la journée" + "all_day": "Toute la journée", + "collection_no_start_end_date": "L'ajout d'une date de début et de fin à la collection débloquera les fonctionnalités de planification de l'itinéraire dans la page de collection.", + "date_itinerary": "Itinéraire de date", + "no_ordered_items": "Ajoutez des articles avec des dates à la collection pour les voir ici.", + "ordered_itinerary": "Itinéraire ordonné" }, "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 216d1210..87c963fc 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -248,7 +248,11 @@ "welcome_map_info": "Avventure pubbliche su questo server", "reservation_number": "Numero di prenotazione", "open_in_maps": "Aperto in mappe", - "all_day": "Tutto il giorno" + "all_day": "Tutto il giorno", + "collection_no_start_end_date": "L'aggiunta di una data di inizio e fine alla raccolta sbloccherà le funzionalità di pianificazione dell'itinerario nella pagina di raccolta.", + "date_itinerary": "Itinerario della data", + "no_ordered_items": "Aggiungi articoli con date alla collezione per vederli qui.", + "ordered_itinerary": "Itinerario ordinato" }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json index 2099c0cf..dda69626 100644 --- a/frontend/src/locales/ko.json +++ b/frontend/src/locales/ko.json @@ -248,7 +248,11 @@ "reservation_number": "예약 번호", "welcome_map_info": "이 서버의 공개 모험", "open_in_maps": "지도에서 열립니다", - "all_day": "하루 종일" + "all_day": "하루 종일", + "collection_no_start_end_date": "컬렉션에 시작 및 종료 날짜를 추가하면 컬렉션 페이지에서 여정 계획 기능이 잠금 해제됩니다.", + "date_itinerary": "날짜 일정", + "no_ordered_items": "컬렉션에 날짜가있는 항목을 추가하여 여기에서 확인하십시오.", + "ordered_itinerary": "주문한 여정" }, "auth": { "both_passwords_required": "두 암호 모두 필요합니다", diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index 6dbf2d78..7cb51dcc 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -248,7 +248,11 @@ "price": "Prijs", "region": "Regio", "open_in_maps": "Open in kaarten", - "all_day": "De hele dag" + "all_day": "De hele dag", + "collection_no_start_end_date": "Als u een start- en einddatum aan de collectie toevoegt, ontgrendelt u de functies van de planning van de route ontgrendelen in de verzamelpagina.", + "date_itinerary": "Datumroute", + "no_ordered_items": "Voeg items toe met datums aan de collectie om ze hier te zien.", + "ordered_itinerary": "Besteld reisschema" }, "home": { "desc_1": "Ontdek, plan en verken met gemak", diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index 3eb73ac9..c4cd9141 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -296,7 +296,11 @@ "reservation_number": "Numer rezerwacji", "welcome_map_info": "Publiczne przygody na tym serwerze", "open_in_maps": "Otwarte w mapach", - "all_day": "Cały dzień" + "all_day": "Cały dzień", + "collection_no_start_end_date": "Dodanie daty rozpoczęcia i końca do kolekcji odblokuje funkcje planowania planu podróży na stronie kolekcji.", + "date_itinerary": "Trasa daty", + "no_ordered_items": "Dodaj przedmioty z datami do kolekcji, aby je zobaczyć tutaj.", + "ordered_itinerary": "Zamówiono trasę" }, "worldtravel": { "country_list": "Lista krajów", diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 0baff34d..5efbf639 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -248,7 +248,11 @@ "region": "Område", "reservation_number": "Bokningsnummer", "open_in_maps": "Kappas in", - "all_day": "Hela dagen" + "all_day": "Hela dagen", + "collection_no_start_end_date": "Att lägga till ett start- och slutdatum till samlingen kommer att låsa upp planeringsfunktioner för resplan på insamlingssidan.", + "date_itinerary": "Datum resplan", + "no_ordered_items": "Lägg till objekt med datum i samlingen för att se dem här.", + "ordered_itinerary": "Beställd resplan" }, "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 dfd5e8e0..84caea81 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -296,7 +296,11 @@ "price": "价格", "reservation_number": "预订号", "open_in_maps": "在地图上打开", - "all_day": "整天" + "all_day": "整天", + "collection_no_start_end_date": "在集合页面中添加开始日期和结束日期将在“收集”页面中解锁行程计划功能。", + "date_itinerary": "日期行程", + "no_ordered_items": "将带有日期的项目添加到集合中,以便在此处查看它们。", + "ordered_itinerary": "订购了行程" }, "auth": { "forgot_password": "忘记密码?", diff --git a/frontend/src/routes/collections/+page.server.ts b/frontend/src/routes/collections/+page.server.ts index 20e2c401..0c9a24be 100644 --- a/frontend/src/routes/collections/+page.server.ts +++ b/frontend/src/routes/collections/+page.server.ts @@ -208,7 +208,7 @@ export const actions: Actions = { const order_direction = formData.get('order_direction') as string; const order_by = formData.get('order_by') as string; - console.log(order_direction, order_by); + // console.log(order_direction, order_by); let adventures: Adventure[] = []; @@ -242,7 +242,7 @@ export const actions: Actions = { previous = res.previous; count = res.count; adventures = [...adventures, ...visited]; - console.log(next, previous, count); + // console.log(next, previous, count); } return { diff --git a/frontend/src/routes/collections/+page.svelte b/frontend/src/routes/collections/+page.svelte index 171a5d4b..6eddc70e 100644 --- a/frontend/src/routes/collections/+page.svelte +++ b/frontend/src/routes/collections/+page.svelte @@ -15,8 +15,6 @@ let collections: Collection[] = data.props.adventures || []; - let currentSort = { attribute: 'name', order: 'asc' }; - let newType: string = ''; let resultsPerPage: number = 25; @@ -235,17 +233,36 @@ aria-label={$t(`adventures.descending`)} /> </div> + <p class="text-lg font-semibold mt-2 mb-2">{$t('adventures.order_by')}</p> + <div class="join"> + <input + class="join-item btn btn-neutral" + type="radio" + name="order_by" + id="upated_at" + value="upated_at" + aria-label={$t('adventures.updated')} + checked + /> + <input + class="join-item btn btn-neutral" + type="radio" + name="order_by" + id="start_date" + value="start_date" + aria-label={$t('adventures.start_date')} + /> + <input + class="join-item btn btn-neutral" + type="radio" + name="order_by" + id="name" + value="name" + aria-label={$t('adventures.name')} + /> + </div> <br /> - <input - type="radio" - name="order_by" - id="name" - class="radio radio-primary" - checked - value="name" - hidden - /> <button type="submit" class="btn btn-success btn-primary mt-4" >{$t(`adventures.sort`)}</button >