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} + + +