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');
- });
diff --git a/frontend/src/lib/components/ImmichSelect.svelte b/frontend/src/lib/components/ImmichSelect.svelte
index d16fa7d..690c0f0 100644
--- a/frontend/src/lib/components/ImmichSelect.svelte
+++ b/frontend/src/lib/components/ImmichSelect.svelte
@@ -3,150 +3,196 @@
import { t } from 'svelte-i18n';
import ImmichLogo from '$lib/assets/immich.svg';
import Upload from '~icons/mdi/upload';
- import type { Location, ImmichAlbum } from '$lib/types';
+ import type { ImmichAlbum } from '$lib/types';
import { debounce } from '$lib';
+ // Props
+ export let copyImmichLocally: boolean = false;
+ export let objectId: string = '';
+ export let contentType: string = 'location';
+ export let defaultDate: string = '';
+
+ // Component state
let immichImages: any[] = [];
let immichSearchValue: string = '';
let searchCategory: 'search' | 'date' | 'album' = 'date';
let immichError: string = '';
let immichNextURL: string = '';
let loading = false;
-
- export let location: Location | null = null;
- export let copyImmichLocally: boolean = false;
+ let albums: ImmichAlbum[] = [];
+ let currentAlbum: string = '';
+ let selectedDate: string = defaultDate || new Date().toISOString().split('T')[0];
const dispatch = createEventDispatcher();
- let albums: ImmichAlbum[] = [];
- let currentAlbum: string = '';
-
- let selectedDate: string =
- (location as Location | null)?.visits
- .map((v) => new Date(v.end_date || v.start_date))
- .sort((a, b) => +b - +a)[0]
- ?.toISOString()
- ?.split('T')[0] || '';
- if (!selectedDate) {
- selectedDate = new Date().toISOString().split('T')[0];
- }
-
+ // Reactive statements
$: {
if (searchCategory === 'album' && currentAlbum) {
immichImages = [];
fetchAlbumAssets(currentAlbum);
} else if (searchCategory === 'date' && selectedDate) {
- // Clear album selection when switching to date mode
- if (currentAlbum) {
- currentAlbum = '';
- }
+ clearAlbumSelection();
searchImmich();
} else if (searchCategory === 'search') {
- // Clear album selection when switching to search mode
- if (currentAlbum) {
- currentAlbum = '';
- }
- // Search will be triggered by the form submission or debounced search
+ clearAlbumSelection();
}
}
- async function loadMoreImmich() {
- // The next URL returned by our API is a absolute url to API, but we need to use the relative path, to use the frontend api proxy.
- const url = new URL(immichNextURL);
- immichNextURL = url.pathname + url.search;
- return fetchAssets(immichNextURL, true);
- }
-
- async function saveImmichRemoteUrl(imageId: string) {
- if (!location) {
- console.error('No location provided to save the image URL');
- return;
- }
- let res = await fetch('/api/images', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- immich_id: imageId,
- object_id: location.id,
- content_type: 'location'
- })
- });
- if (res.ok) {
- let data = await res.json();
- if (!data.image) {
- console.error('No image data returned from the server');
- immichError = $t('immich.error_saving_image');
- return;
- }
- dispatch('remoteImmichSaved', data);
- } else {
- let errorData = await res.json();
- console.error('Error saving image URL:', errorData);
- immichError = $t(errorData.message || 'immich.error_saving_image');
+ // Helper functions
+ function clearAlbumSelection() {
+ if (currentAlbum) {
+ currentAlbum = '';
}
}
- async function fetchAssets(url: string, usingNext = false) {
- loading = true;
- try {
- let res = await fetch(url);
- immichError = '';
- if (!res.ok) {
- let data = await res.json();
- let errorMessage = data.message;
- console.error('Error in handling fetchAsstes', errorMessage);
- immichError = $t(data.code);
- } else {
- let data = await res.json();
- if (data.results && data.results.length > 0) {
- if (usingNext) {
- immichImages = [...immichImages, ...data.results];
- } else {
- immichImages = data.results;
- }
- } else {
- immichError = $t('immich.no_items_found');
- }
+ function buildQueryParams(): string {
+ const params = new URLSearchParams();
- immichNextURL = data.next || '';
- }
- } finally {
- loading = false;
- }
- }
-
- async function fetchAlbumAssets(album_id: string) {
- return fetchAssets(`/api/integrations/immich/albums/${album_id}`);
- }
-
- onMount(async () => {
- let res = await fetch('/api/integrations/immich/albums');
- if (res.ok) {
- let data = await res.json();
- albums = data;
- }
- });
-
- function buildQueryParams() {
- let params = new URLSearchParams();
if (immichSearchValue && searchCategory === 'search') {
params.append('query', immichSearchValue);
} else if (selectedDate && searchCategory === 'date') {
params.append('date', selectedDate);
}
+
return params.toString();
}
+ // API functions
+ async function fetchAssets(url: string, usingNext = false): Promise
{
+ loading = true;
+ immichError = '';
+
+ try {
+ const res = await fetch(url);
+
+ if (!res.ok) {
+ const data = await res.json();
+ console.error('Error in fetchAssets:', data.message);
+ immichError = $t(data.code || 'immich.fetch_error');
+ return;
+ }
+
+ const data = await res.json();
+
+ if (data.results && data.results.length > 0) {
+ if (usingNext) {
+ immichImages = [...immichImages, ...data.results];
+ } else {
+ immichImages = data.results;
+ }
+ immichNextURL = data.next || '';
+ } else {
+ immichError = $t('immich.no_items_found');
+ immichNextURL = '';
+ }
+ } catch (error) {
+ console.error('Error fetching assets:', error);
+ immichError = $t('immich.fetch_error');
+ } finally {
+ loading = false;
+ }
+ }
+
+ async function fetchAlbumAssets(albumId: string): Promise {
+ return fetchAssets(`/api/integrations/immich/albums/${albumId}`);
+ }
+
+ async function loadMoreImmich(): Promise {
+ if (!immichNextURL) return;
+
+ // Convert absolute URL to relative path for frontend API proxy
+ const url = new URL(immichNextURL);
+ const relativePath = url.pathname + url.search;
+
+ return fetchAssets(relativePath, true);
+ }
+
+ async function saveImmichRemoteUrl(imageId: string): Promise {
+ if (!objectId) {
+ console.error('No object ID provided to save the image URL');
+ immichError = $t('immich.error_no_object_id');
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/images', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ immich_id: imageId,
+ object_id: objectId,
+ content_type: contentType
+ })
+ });
+
+ if (res.ok) {
+ const data = await res.json();
+
+ if (!data.image) {
+ console.error('No image data returned from the server');
+ immichError = $t('immich.error_saving_image');
+ return;
+ }
+
+ dispatch('remoteImmichSaved', data);
+ } else {
+ const errorData = await res.json();
+ console.error('Error saving image URL:', errorData);
+ immichError = $t(errorData.message || 'immich.error_saving_image');
+ }
+ } catch (error) {
+ console.error('Error in saveImmichRemoteUrl:', error);
+ immichError = $t('immich.error_saving_image');
+ }
+ }
+
+ // Event handlers
const searchImmich = debounce(() => {
_searchImmich();
- }, 500); // Debounce the search function to avoid multiple requests on every key press
+ }, 500);
- async function _searchImmich() {
+ async function _searchImmich(): Promise {
immichImages = [];
return fetchAssets(`/api/integrations/immich/search/?${buildQueryParams()}`);
}
+
+ function handleSearchCategoryChange(category: 'search' | 'date' | 'album') {
+ searchCategory = category;
+ immichError = '';
+
+ if (category !== 'album') {
+ clearAlbumSelection();
+ }
+ }
+
+ function handleImageSelect(image: any) {
+ const currentDomain = window.location.origin;
+ const fullUrl = `${currentDomain}/immich/${image.id}`;
+
+ if (copyImmichLocally) {
+ dispatch('fetchImage', fullUrl);
+ } else {
+ saveImmichRemoteUrl(image.id);
+ }
+ }
+
+ // Lifecycle
+ onMount(async () => {
+ try {
+ const res = await fetch('/api/integrations/immich/albums');
+
+ if (res.ok) {
+ const data = await res.json();
+ albums = data;
+ } else {
+ console.warn('Failed to fetch Immich albums');
+ }
+ } catch (error) {
+ console.error('Error fetching albums:', error);
+ }
+ });
@@ -163,31 +209,28 @@
-
+
{#if searchCategory === 'search'}
{:else if searchCategory === 'date'}
-
{:else if searchCategory === 'album'}
-
+
{$t('immich.select_album')}
-