From c6fa603a93e708a4ba3cb04db9c837f9c8456947 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 2 Jan 2025 23:25:58 -0500 Subject: [PATCH] feat: add primary image functionality to AdventureImage model and update related components --- .../0017_adventureimage_is_primary.py | 18 ++++++++ backend/server/adventures/models.py | 1 + backend/server/adventures/serializers.py | 2 +- backend/server/adventures/views.py | 22 ++++++++++ .../src/lib/components/AdventureModal.svelte | 44 +++++++++++++++++-- .../src/lib/components/CardCarousel.svelte | 19 +++++++- frontend/src/lib/types.ts | 1 + .../src/routes/adventures/[id]/+page.svelte | 10 +++++ 8 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 backend/server/adventures/migrations/0017_adventureimage_is_primary.py diff --git a/backend/server/adventures/migrations/0017_adventureimage_is_primary.py b/backend/server/adventures/migrations/0017_adventureimage_is_primary.py new file mode 100644 index 0000000..9a920a3 --- /dev/null +++ b/backend/server/adventures/migrations/0017_adventureimage_is_primary.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.8 on 2025-01-03 04:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0016_alter_adventureimage_image'), + ] + + operations = [ + migrations.AddField( + model_name='adventureimage', + name='is_primary', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index 68c8ad6..c77bc4d 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -280,6 +280,7 @@ class AdventureImage(models.Model): upload_to=PathAndRename('images/') # Use the callable class here ) adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE) + is_primary = models.BooleanField(default=False) def __str__(self): return self.image.url diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 45a2141..d78e93d 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -8,7 +8,7 @@ from main.utils import CustomModelSerializer class AdventureImageSerializer(CustomModelSerializer): class Meta: model = AdventureImage - fields = ['id', 'image', 'adventure'] + fields = ['id', 'image', 'adventure', 'is_primary'] read_only_fields = ['id'] def to_representation(self, instance): diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index b1c3956..3ee3e79 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -1032,7 +1032,29 @@ class AdventureImageViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['post']) def image_delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) + + @action(detail=True, methods=['post']) + def toggle_primary(self, request, *args, **kwargs): + # Makes the image the primary image for the adventure, if there is already a primary image linked to the adventure, it is set to false and the new image is set to true. make sure that the permission is set to the owner of the adventure + if not request.user.is_authenticated: + return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED) + + instance = self.get_object() + adventure = instance.adventure + if adventure.user_id != request.user: + return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) + + # Check if the image is already the primary image + if instance.is_primary: + return Response({"error": "Image is already the primary image"}, status=status.HTTP_400_BAD_REQUEST) + + # Set the current primary image to false + AdventureImage.objects.filter(adventure=adventure, is_primary=True).update(is_primary=False) + # Set the new image to true + instance.is_primary = True + instance.save() + return Response({"success": "Image set as primary image"}) def create(self, request, *args, **kwargs): if not request.user.is_authenticated: diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 550c419..138ff3d 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -21,7 +21,7 @@ let query: string = ''; let places: OpenStreetMapPlace[] = []; - let images: { id: string; image: string }[] = []; + let images: { id: string; image: string; is_primary: boolean }[] = []; let warningMessage: string = ''; let constrainDates: boolean = false; @@ -34,6 +34,9 @@ import MarkdownEditor from './MarkdownEditor.svelte'; import ImmichSelect from './ImmichSelect.svelte'; + import Star from '~icons/mdi/star'; + import Crown from '~icons/mdi/crown'; + let wikiError: string = ''; let noPlaces: boolean = false; @@ -179,6 +182,25 @@ } } + async function makePrimaryImage(image_id: string) { + let res = await fetch(`/api/images/${image_id}/toggle_primary`, { + method: 'POST' + }); + if (res.ok) { + images = images.map((image) => { + if (image.id === image_id) { + image.is_primary = true; + } else { + image.is_primary = false; + } + return image; + }); + adventure.images = images; + } else { + console.error('Error in makePrimaryImage:', res); + } + } + async function fetchImage() { try { let res = await fetch(url); @@ -208,7 +230,8 @@ // Assuming the first object in the array is the new image let newImage = { id: rawData[1], - image: rawData[2] // This is the URL for the image + image: rawData[2], // This is the URL for the image + is_primary: false }; console.log('New Image:', newImage); @@ -249,7 +272,7 @@ if (res2.ok) { let newData = deserialize(await res2.text()) as { data: { id: string; image: string } }; console.log(newData); - let newImage = { id: newData.data.id, image: newData.data.image }; + let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false }; console.log(newImage); images = [...images, newImage]; adventure.images = images; @@ -1038,6 +1061,21 @@ it would also work to just use on:click on the MapLibre component itself. --> > ✕ + {#if !image.is_primary} + + {:else} + + +
+ +
+ {/if} {image.id} - adventure.images.map((image) => ({ image: image.image, adventure: adventure })) + adventure.images.map((image) => ({ + image: image.image, + adventure: adventure, + is_primary: image.is_primary + })) ); $: { @@ -18,6 +22,19 @@ } } + $: { + // sort so that any image in adventure_images .is_primary is first + adventure_images.sort((a, b) => { + if (a.is_primary && !b.is_primary) { + return -1; + } else if (!a.is_primary && b.is_primary) { + return 1; + } else { + return 0; + } + }); + } + function changeSlide(direction: string) { if (direction === 'next' && currentSlide < adventure_images.length - 1) { currentSlide = currentSlide + 1; diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index a2e6d3c..ea9e1fb 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -24,6 +24,7 @@ export type Adventure = { images: { id: string; image: string; + is_primary: boolean; }[]; visits: { id: string; diff --git a/frontend/src/routes/adventures/[id]/+page.svelte b/frontend/src/routes/adventures/[id]/+page.svelte index f151bb4..21b622f 100644 --- a/frontend/src/routes/adventures/[id]/+page.svelte +++ b/frontend/src/routes/adventures/[id]/+page.svelte @@ -34,6 +34,16 @@ onMount(() => { if (data.props.adventure) { adventure = data.props.adventure; + // sort so that any image in adventure_images .is_primary is first + adventure.images.sort((a, b) => { + if (a.is_primary && !b.is_primary) { + return -1; + } else if (!a.is_primary && b.is_primary) { + return 1; + } else { + return 0; + } + }); } else { notFound = true; }