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')}