diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 8c2d121..231dbe1 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -230,7 +230,7 @@ class LocationSerializer(CustomModelSerializer): return obj.is_visited_status() def create(self, validated_data): - visits_data = validated_data.pop('visits', None) + visits_data = validated_data.pop('visits', []) category_data = validated_data.pop('category', None) collections_data = validated_data.pop('collections', []) diff --git a/backend/server/adventures/views/location_image_view.py b/backend/server/adventures/views/location_image_view.py index 7cc0ec8..c81b75c 100644 --- a/backend/server/adventures/views/location_image_view.py +++ b/backend/server/adventures/views/location_image_view.py @@ -262,24 +262,38 @@ class ContentImageViewSet(viewsets.ModelViewSet): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def _create_standard_image(self, request, content_object, content_type, object_id): - """Handle standard image creation""" - # Add content type and object ID to request data - request_data = request.data.copy() - request_data['content_type'] = content_type.id - request_data['object_id'] = object_id + """Handle standard image creation without deepcopy issues""" - # Create serializer with modified data + # Get uploaded image file safely + image_file = request.FILES.get('image') + if not image_file: + return Response({"error": "No image uploaded"}, status=status.HTTP_400_BAD_REQUEST) + + # Build a clean dict for serializer input + request_data = { + 'content_type': content_type.id, + 'object_id': object_id, + } + + # Optionally add other fields (e.g., caption, alt text) from request.data + for key in ['caption', 'alt_text', 'description']: # update as needed + if key in request.data: + request_data[key] = request.data[key] + + # Create and validate serializer serializer = self.get_serializer(data=request_data) serializer.is_valid(raise_exception=True) - - # Save the image + + # Save with image passed explicitly serializer.save( - user=content_object.user if hasattr(content_object, 'user') else request.user, + user=getattr(content_object, 'user', request.user), content_type=content_type, - object_id=object_id + object_id=object_id, + image=image_file ) - + return Response(serializer.data, status=status.HTTP_201_CREATED) + def perform_create(self, serializer): # The content_type and object_id are already set in the create method diff --git a/frontend/src/lib/components/CategoryDropdown.svelte b/frontend/src/lib/components/CategoryDropdown.svelte index 246153e..381db3f 100644 --- a/frontend/src/lib/components/CategoryDropdown.svelte +++ b/frontend/src/lib/components/CategoryDropdown.svelte @@ -3,7 +3,6 @@ import type { Category } from '$lib/types'; import { t } from 'svelte-i18n'; - export let categories: Category[] = []; export let selected_category: Category | null = null; export let searchTerm: string = ''; let new_category: Category = { @@ -15,6 +14,12 @@ num_locations: 0 }; + $: { + console.log('Selected category changed:', selected_category); + } + + let categories: Category[] = []; + let isOpen: boolean = false; let isEmojiPickerVisible: boolean = false; @@ -45,7 +50,15 @@ let dropdownRef: HTMLDivElement; onMount(() => { - categories = categories.sort((a, b) => (b.num_locations || 0) - (a.num_locations || 0)); + const loadData = async () => { + await import('emoji-picker-element'); + let res = await fetch('/api/categories'); + categories = await res.json(); + categories = categories.sort((a, b) => (b.num_locations || 0) - (a.num_locations || 0)); + }; + + loadData(); + const handleClickOutside = (event: MouseEvent) => { if (dropdownRef && !dropdownRef.contains(event.target as Node)) { isOpen = false; @@ -56,9 +69,6 @@ document.removeEventListener('click', handleClickOutside); }; }); - onMount(async () => { - await import('emoji-picker-element'); - }); + + +
+
+
+
+ +
+

{$t('adventures.tags')} ({location.tags?.length || 0})

+
+
+ + + + +
+
+
+ + +
+
+
+
+ +
+

Location & Map

+
+ +
+ +
+ +
+ + +
+ + +
+ +
+
+ +
+ + {#if searchQuery && !selectedLocation} + + {/if} +
+
+ + + {#if isSearching} +
+ + Searching... +
+ {:else if searchResults.length > 0} +
+ +
+ {#each searchResults as result} + + {/each} +
+
+ {/if} + + +
+
OR
+
+ + + + + {#if selectedLocation && selectedMarker} +
+
+
+
+ +
+
+

Location Selected

+

{selectedLocation.name}

+

+ {selectedMarker.lat.toFixed(6)}, {selectedMarker.lng.toFixed(6)} +

+ + + {#if locationData?.city || locationData?.region || locationData?.country} +
+ {#if locationData.city} +
+ πŸ™οΈ {locationData.city.name} +
+ {/if} + {#if locationData.region} +
+ πŸ—ΊοΈ {locationData.region.name} +
+ {/if} + {#if locationData.country} +
+ 🌎 {locationData.country.name} +
+ {/if} +
+ {/if} +
+ +
+
+
+ {/if} +
+ + +
+
+ + {#if isReverseGeocoding} +
+ + Getting location details... +
+ {/if} +
+ +
+ + + + {#if selectedMarker} + + + + {/if} + +
+ + {#if !selectedMarker} +

+ Click on the map to select a location +

+ {/if} +
+
+
+
+ + +
+ + +
+ + diff --git a/frontend/src/lib/components/locations/LocationMedia.svelte b/frontend/src/lib/components/locations/LocationMedia.svelte new file mode 100644 index 0000000..a32df67 --- /dev/null +++ b/frontend/src/lib/components/locations/LocationMedia.svelte @@ -0,0 +1,761 @@ + + +
+
+ +
+
+
+
+ +
+

Image Management

+
+ + +
+ +
+

+ {$t('adventures.upload_from_device')} +

+ +
+ + +
+

+ {$t('adventures.upload_from_url')} +

+
+ + +
+ {#if imageError} +
+ {imageError} +
+ {/if} +
+ + +
+

+ {$t('adventures.wikipedia')} +

+
+ + +
+ {#if wikiImageError} +
+ {wikiImageError} +
+ {/if} +
+ + + {#if immichIntegration} +
+

Immich Integration

+ { + url = e.detail; + handleUrlUpload(); + }} + on:remoteImmichSaved={handleImmichImageSaved} + /> +
+ {/if} +
+ + + {#if images.length > 0} +
Current Images
+
+ {#each images as image (image.id)} +
+
+ Uploaded content +
+ + +
+ {#if !image.is_primary} + + {/if} + + +
+ + + {#if image.is_primary} +
+ +
+ {/if} +
+ {/each} +
+ {:else} +
+
No images uploaded yet
+
+ Upload your first image using one of the options above +
+
+ {/if} +
+
+ + +
+
+
+
+ +
+

Attachment Management

+
+ + +
+ +
+

+ {$t('adventures.upload_attachment')} +

+
+ + + +
+ {#if attachmentError} +
+ {attachmentError} +
+ {/if} +
+ + +
+ +
+
Supported file types:
+
+ GPX, KML, PDF, DOC, TXT, JSON, CSV, XLSX and more +
+
+
+
+ + + {#if attachments.length > 0} +
Current Attachments
+
+ {#each attachments as attachment (attachment.id)} +
+ {#if attachmentToEdit?.id === attachment.id} + +
+
+ + Editing +
+ +
+ + +
+
+ {:else} + +
+
+
+ +
+
+
{attachment.name}
+
+ {attachment.extension.toUpperCase()} +
+
+
+ + +
+ + +
+
+ {/if} +
+ {/each} +
+ {:else} +
+
No attachments uploaded yet
+
+ Upload your first attachment using the options above +
+
+ {/if} +
+
+ + +
+
+
+
+ +
+

Trails Management

+
+

+ You can manage trails associated with this location in the Trails section. +

+

+ Coming soon: Create, edit, and delete trails directly from this section. +

+
+
+ + +
+ + + +
+
+
diff --git a/frontend/src/lib/components/locations/LocationQuickStart.svelte b/frontend/src/lib/components/locations/LocationQuickStart.svelte new file mode 100644 index 0000000..8167096 --- /dev/null +++ b/frontend/src/lib/components/locations/LocationQuickStart.svelte @@ -0,0 +1,462 @@ + + +
+ +
+
+
+ +
+ + +
+
+ +
+ + {#if searchQuery && !selectedLocation} + + {/if} +
+
+ + + {#if isSearching} +
+ + Searching... +
+ {:else if searchResults.length > 0} +
+ + +
+ {#each searchResults as result} + + {/each} +
+
+ {/if} + + +
+
OR
+
+ + +
+
+
+ + +
+
+
+

+ + {$t('adventures.select_on_map') || 'Select on Map'} +

+ {#if selectedMarker} + + {/if} +
+ + {#if !selectedMarker} +

+ {$t('adventures.click_map') || 'Click on the map to select a location'} +

+ {/if} + + {#if isReverseGeocoding} +
+ + Getting location details... +
+ {/if} + +
+ + + + {#if selectedMarker} + + + + {/if} + +
+
+
+ + + {#if selectedLocation && selectedMarker} +
+
+
+
+ +
+
+

Location Selected

+

{selectedLocation.name}

+

+ {selectedMarker.lat.toFixed(6)}, {selectedMarker.lng.toFixed(6)} +

+ {#if selectedLocation.category} +

+ {selectedLocation.category} β€’ {selectedLocation.type || 'location'} +

+ {/if} + + + {#if locationData?.city || locationData?.region || locationData?.country} +
+ {#if locationData.city} +
+ πŸ™οΈ {locationData.city.name} +
+ {/if} + {#if locationData.region} +
+ πŸ—ΊοΈ {locationData.region.name} +
+ {/if} + {#if locationData.country} +
+ 🌎 {locationData.country.name} +
+ {/if} +
+ {/if} + + {#if locationData?.display_name} +

+ {locationData.display_name} +

+ {/if} +
+
+
+
+ {/if} + + +
+ + +
+
diff --git a/frontend/src/lib/components/locations/LocationVisits.svelte b/frontend/src/lib/components/locations/LocationVisits.svelte new file mode 100644 index 0000000..0482a56 --- /dev/null +++ b/frontend/src/lib/components/locations/LocationVisits.svelte @@ -0,0 +1,602 @@ + + +
+
+ +
+
+
+ +
+

{$t('adventures.date_information')}

+
+ +
+ + {#if isExpanded} + +
+
+ +

Settings

+
+ +
+ + {#if type === 'transportation'} +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ {:else} +
+ +
+ +
+
+ {/if} + + +
+
+ + + +
+ + {#if collection?.start_date && collection?.end_date} +
+ + + +
+ {/if} +
+
+
+ + +
+

Date Selection

+ +
+ +
+ + {#if allDay} + + {:else} + + {/if} +
+ + + {#if localStartDate} +
+ + {#if allDay} + + {:else} + + {/if} +
+ {/if} +
+ + + {#if type === 'location'} +
+ + +
+ + +
+ +
+ {/if} +
+ + + {#if !isDateValid} +
+ + Invalid date range - end date must be after start date +
+ {/if} + + + {#if type === 'location'} +
+

+ Visits ({visits?.length || 0}) +

+ + {#if !visits || visits.length === 0} +
+ +

No visits added yet

+

+ Create your first visit by selecting dates above +

+
+ {:else} +
+ {#each visits as visit (visit.id)} +
+
+
+
+ {#if isAllDay(visit.start_date)} + All Day + {:else} + + {/if} +
+ {#if isAllDay(visit.start_date)} + {visit.start_date && typeof visit.start_date === 'string' + ? visit.start_date.split('T')[0] + : ''} + – {visit.end_date && typeof visit.end_date === 'string' + ? visit.end_date.split('T')[0] + : ''} + {:else if 'start_timezone' in visit} + {formatDateInTimezone(visit.start_date, visit.start_timezone)} + – {formatDateInTimezone(visit.end_date, visit.end_timezone)} + {:else if visit.timezone} + {formatDateInTimezone(visit.start_date, visit.timezone)} + – {formatDateInTimezone(visit.end_date, visit.timezone)} + {:else} + {new Date(visit.start_date).toLocaleString()} + – {new Date(visit.end_date).toLocaleString()} + {/if} +
+
+ + {#if visit.notes} +

+ "{visit.notes}" +

+ {/if} +
+ + +
+ + +
+
+
+ {/each} +
+ {/if} +
+ {/if} + {/if} +
+
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index a72c119..4b5aa38 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -279,7 +279,6 @@ export type ImmichAlbum = { export type Attachment = { id: string; file: string; - location: string; extension: string; user: string; name: string; diff --git a/frontend/src/routes/locations/+page.svelte b/frontend/src/routes/locations/+page.svelte index c0cf344..121edf1 100644 --- a/frontend/src/routes/locations/+page.svelte +++ b/frontend/src/routes/locations/+page.svelte @@ -20,6 +20,7 @@ import Star from '~icons/mdi/star'; import Tag from '~icons/mdi/tag'; import Compass from '~icons/mdi/compass'; + import NewLocationModal from '$lib/components/NewLocationModal.svelte'; export let data: any; @@ -34,6 +35,19 @@ is_visited: 'all' }; + let locationBeingUpdated: Location | undefined = undefined; + + // Sync the locationBeingUpdated with the adventures array + $: { + if (locationBeingUpdated && locationBeingUpdated.id) { + const index = adventures.findIndex((adventure) => adventure.id === locationBeingUpdated!.id); + if (index !== -1) { + adventures[index] = { ...locationBeingUpdated }; + adventures = adventures; // Trigger reactivity + } + } + } + let resultsPerPage: number = 25; let count = data.props.count || 0; let totalPages = Math.ceil(count / resultsPerPage); @@ -141,7 +155,6 @@ } else { adventures = [event.detail, ...adventures]; } - isLocationModalOpen = false; } function editAdventure(event: CustomEvent) { @@ -168,11 +181,18 @@ {#if isLocationModalOpen} - (isLocationModalOpen = false)} on:save={saveOrCreate} user={data.user} + /> --> + (isLocationModalOpen = false)} + on:save={saveOrCreate} + user={data.user} + locationToEdit={adventureToEdit} + bind:location={locationBeingUpdated} /> {/if}