diff --git a/frontend/src/app.html b/frontend/src/app.html index 0b2e937c..b5179723 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -8,8 +8,7 @@ - - + { + if (!img || !img.source) return false; + if (seen.has(img.source)) return false; + seen.add(img.source); + return true; + }); + } } catch (error) { wikiImageError = $t('adventures.wiki_image_error'); addToast('error', $t('adventures.image_upload_error')); @@ -366,7 +374,7 @@
- {#each wikiImageResults as result (result.source)} + {#each wikiImageResults as result, i (result.source + '-' + i)}
diff --git a/frontend/src/lib/components/locations/CollectionItineraryPlanner.svelte b/frontend/src/lib/components/locations/CollectionItineraryPlanner.svelte index 71695f59..30f61fa4 100644 --- a/frontend/src/lib/components/locations/CollectionItineraryPlanner.svelte +++ b/frontend/src/lib/components/locations/CollectionItineraryPlanner.svelte @@ -119,13 +119,69 @@ isLocationModalOpen = true; } - function handleDeleteLocation(event: CustomEvent) { - // remove locally deleted location from itinerary view and list - const deletedLocation = event.detail; - collection.locations = collection.locations?.filter((loc) => loc.id !== deletedLocation.id); - collection.itinerary = collection.itinerary?.filter( - (it) => !(it.item?.type === 'location' && it.object_id === deletedLocation.id) - ); + function handleItemDelete(event: CustomEvent) { + const payload = event.detail; + + // Support both cases: + // 1) Card components dispatch a primitive id (string/number) when deleting the underlying object + // 2) Some callers may dispatch a full itinerary item object + if (typeof payload === 'string' || typeof payload === 'number') { + const objectId = payload; + + // Remove any itinerary entries that reference this object + collection.itinerary = collection.itinerary?.filter( + (it) => String(it.object_id) !== String(objectId) + ); + + // Remove the object from all possible collections (location/transportation/lodging/note/checklist) + if (collection.locations) { + collection.locations = collection.locations.filter( + (loc) => String(loc.id) !== String(objectId) + ); + } + if (collection.transportations) { + collection.transportations = collection.transportations.filter( + (t) => String(t.id) !== String(objectId) + ); + } + if (collection.lodging) { + collection.lodging = collection.lodging.filter((l) => String(l.id) !== String(objectId)); + } + if (collection.notes) { + collection.notes = collection.notes.filter((n) => String(n.id) !== String(objectId)); + } + if (collection.checklists) { + collection.checklists = collection.checklists.filter( + (c) => String(c.id) !== String(objectId) + ); + } + + // Re-group days and return + days = groupItemsByDay(collection); + return; + } + + // Otherwise expect a full itinerary-like object + const itemToDelete = payload as CollectionItineraryItem; + collection.itinerary = collection.itinerary?.filter((it) => it.id !== itemToDelete.id); + // Also remove the associated object from the collection + const objectType = itemToDelete.item?.type || ''; + if (objectType === 'location') { + collection.locations = collection.locations?.filter( + (loc) => loc.id !== itemToDelete.object_id + ); + } else if (objectType === 'transportation') { + collection.transportations = collection.transportations?.filter( + (t) => t.id !== itemToDelete.object_id + ); + } else if (objectType === 'lodging') { + collection.lodging = collection.lodging?.filter((l) => l.id !== itemToDelete.object_id); + } else if (objectType === 'note') { + collection.notes = collection.notes?.filter((n) => n.id !== itemToDelete.object_id); + } else if (objectType === 'checklist') { + collection.checklists = collection.checklists?.filter((c) => c.id !== itemToDelete.object_id); + } + days = groupItemsByDay(collection); } let locationBeingUpdated: Location | null = null; @@ -142,6 +198,8 @@ // When opening a "create new item" modal we store the target date here let pendingAddDate: string | null = null; + // Track if we've already added this location to the itinerary + let addedToItinerary: Set = new Set(); // Sync the locationBeingUpdated with the collection.locations array $: if (locationBeingUpdated && locationBeingUpdated.id && collection) { @@ -168,9 +226,15 @@ // If a new location was just created and we have a pending add-date, // attach it to that date in the itinerary. - $: if (locationBeingUpdated?.id && pendingAddDate) { + $: if ( + locationBeingUpdated?.id && + pendingAddDate && + !addedToItinerary.has(locationBeingUpdated.id) + ) { addItineraryItemForObject('location', locationBeingUpdated.id, pendingAddDate); - pendingAddDate = null; + // Mark this location as added to prevent duplicates + addedToItinerary.add(locationBeingUpdated.id); + addedToItinerary = addedToItinerary; // trigger reactivity } /** @@ -543,11 +607,17 @@ {#if isLocationModalOpen} (isLocationModalOpen = false)} + on:close={() => { + isLocationModalOpen = false; + pendingAddDate = null; + addedToItinerary.clear(); + addedToItinerary = addedToItinerary; + }} {user} {locationToEdit} bind:location={locationBeingUpdated} {collection} + initialVisitDate={pendingAddDate} /> {/if} @@ -898,7 +968,7 @@ @@ -919,6 +990,7 @@ {user} {collection} itineraryItem={item} + on:delete={handleItemDelete} on:removeFromItinerary={handleRemoveItineraryItem} /> {:else if objectType === 'note'} @@ -927,6 +999,7 @@ note={resolvedObj} {user} {collection} + on:delete={handleItemDelete} itineraryItem={item} on:removeFromItinerary={handleRemoveItineraryItem} /> @@ -936,6 +1009,7 @@ checklist={resolvedObj} {user} {collection} + on:delete={handleItemDelete} itineraryItem={item} on:removeFromItinerary={handleRemoveItineraryItem} /> diff --git a/frontend/src/lib/components/locations/LocationVisits.svelte b/frontend/src/lib/components/locations/LocationVisits.svelte index 4bc7fce5..7eedb302 100644 --- a/frontend/src/lib/components/locations/LocationVisits.svelte +++ b/frontend/src/lib/components/locations/LocationVisits.svelte @@ -45,6 +45,7 @@ export let objectId: string; export let trails: Trail[] = []; export let measurementSystem: 'metric' | 'imperial' = 'metric'; + export let initialVisitDate: string | null = null; // Used to pre-fill visit date when adding from itinerary planner const dispatch = createEventDispatcher(); @@ -228,7 +229,7 @@ }).localDate; } - async function addVisit() { + async function addVisit(isAuto: boolean = false) { // If editing an existing visit, patch instead of creating new if (visitIdEditing) { const response = await fetch(`/api/visits/${visitIdEditing}/`, { @@ -279,12 +280,14 @@ } } - // Reset form fields - note = ''; - localStartDate = ''; - localEndDate = ''; - utcStartDate = null; - utcEndDate = null; + // Reset form fields. If this call was an auto-generated visit, allow clearing even if initialVisitDate is set + if (!initialVisitDate || isAuto) { + note = ''; + localStartDate = ''; + localEndDate = ''; + utcStartDate = null; + utcEndDate = null; + } } // Activity management functions @@ -694,6 +697,43 @@ } catch { stravaEnabled = false; } + + // If initialVisitDate is provided and a visit on that date doesn't exist, create and upload a new all day visit on that date + if (initialVisitDate) { + const targetDate = initialVisitDate.split('T')[0]; // Ensure we have just YYYY-MM-DD + + // Check if any visit already exists for this date + const visitExists = visits?.some((visit) => { + const visitStart = visit.start_date.split('T')[0]; + const visitEnd = visit.end_date.split('T')[0]; + return targetDate >= visitStart && targetDate <= visitEnd; + }); + + if (!visitExists) { + // Set up an all-day visit for this date + allDay = true; + localStartDate = targetDate; + localEndDate = targetDate; + + // Convert to UTC dates + utcStartDate = updateUTCDate({ + localDate: localStartDate, + timezone: selectedStartTimezone, + allDay: true + }).utcDate; + + utcEndDate = updateUTCDate({ + localDate: localEndDate, + timezone: selectedStartTimezone, + allDay: true + }).utcDate; + + // Automatically save the visit + await addVisit(true); + // Clear the initialVisitDate after successful creation + initialVisitDate = null; + } + } }); $: isDateValid = validateDateRange(utcStartDate ?? '', utcEndDate ?? '').valid; @@ -852,7 +892,7 @@ class="btn btn-{typeConfig.color} btn-sm gap-2" type="button" disabled={!localStartDate || !isDateValid} - on:click={addVisit} + on:click={() => addVisit(false)} > {visitIdEditing ? $t('adventures.update_visit') : $t('adventures.add_visit')}