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 0000000..668e968 --- /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 c7f78ca..0d53bc9 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 97dd633..d69466d 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 c8b0743..b9daa0f 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 9012dd9..1d4a516 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 fd21597..70aef59 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 56c84dc..c0d939e 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 25a79de..4e32577 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 3814ec2..bd9b6a4 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 3e6ec37..2523fe5 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 a68ce99..216d121 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 bfe761b..2099c0c 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 4a783ec..6dbf2d7 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 3925de1..3eb73ac 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 c6fd200..0baff34 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 864d699..dfd5e8e 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 17f1ae3..6d88e0e 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 a63dbe5..b6ffac3 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 1321db0..3054687 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 70aef59..af68de2 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 b6ffac3..1a69fdd 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 af68de2..d66abec 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 1e39414..61f0852 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 a099f11..f9291f0 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 1a69fdd..df8ad0a 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 dbd9299..4263aca 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 0000000..aecf431 --- /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 e5cb294..93ec776 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 4e32577..72d1883 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 df8ad0a..4bdba31 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 dad43a1..aeabc14 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 4263aca..4877f62 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 310237f..cfb30f4 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 0000000..8cf0129 --- /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 3054687..85db0a4 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 4bdba31..2578384 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 f0529ee..2c46dc5 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 c0d939e..13a02ad 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 bd9b6a4..c82df3f 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 2523fe5..249243d 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 216d121..87c963f 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 2099c0c..dda6962 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 6dbf2d7..7cb51dc 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 3eb73ac..c4cd914 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 0baff34..5efbf63 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 dfd5e8e..84caea8 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 20e2c40..0c9a24b 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 171a5d4..6eddc70 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 >