mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-22 06:19:38 +02:00
feat: add primary image functionality to AdventureImage model and update related components
This commit is contained in:
parent
991efa8d08
commit
c6fa603a93
8 changed files with 112 additions and 5 deletions
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -280,6 +280,7 @@ class AdventureImage(models.Model):
|
||||||
upload_to=PathAndRename('images/') # Use the callable class here
|
upload_to=PathAndRename('images/') # Use the callable class here
|
||||||
)
|
)
|
||||||
adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE)
|
adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE)
|
||||||
|
is_primary = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.image.url
|
return self.image.url
|
||||||
|
|
|
@ -8,7 +8,7 @@ from main.utils import CustomModelSerializer
|
||||||
class AdventureImageSerializer(CustomModelSerializer):
|
class AdventureImageSerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdventureImage
|
model = AdventureImage
|
||||||
fields = ['id', 'image', 'adventure']
|
fields = ['id', 'image', 'adventure', 'is_primary']
|
||||||
read_only_fields = ['id']
|
read_only_fields = ['id']
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
|
|
|
@ -1032,7 +1032,29 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def image_delete(self, request, *args, **kwargs):
|
def image_delete(self, request, *args, **kwargs):
|
||||||
return self.destroy(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):
|
def create(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
let query: string = '';
|
let query: string = '';
|
||||||
let places: OpenStreetMapPlace[] = [];
|
let places: OpenStreetMapPlace[] = [];
|
||||||
let images: { id: string; image: string }[] = [];
|
let images: { id: string; image: string; is_primary: boolean }[] = [];
|
||||||
let warningMessage: string = '';
|
let warningMessage: string = '';
|
||||||
let constrainDates: boolean = false;
|
let constrainDates: boolean = false;
|
||||||
|
|
||||||
|
@ -34,6 +34,9 @@
|
||||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||||
import ImmichSelect from './ImmichSelect.svelte';
|
import ImmichSelect from './ImmichSelect.svelte';
|
||||||
|
|
||||||
|
import Star from '~icons/mdi/star';
|
||||||
|
import Crown from '~icons/mdi/crown';
|
||||||
|
|
||||||
let wikiError: string = '';
|
let wikiError: string = '';
|
||||||
|
|
||||||
let noPlaces: boolean = false;
|
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() {
|
async function fetchImage() {
|
||||||
try {
|
try {
|
||||||
let res = await fetch(url);
|
let res = await fetch(url);
|
||||||
|
@ -208,7 +230,8 @@
|
||||||
// Assuming the first object in the array is the new image
|
// Assuming the first object in the array is the new image
|
||||||
let newImage = {
|
let newImage = {
|
||||||
id: rawData[1],
|
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);
|
console.log('New Image:', newImage);
|
||||||
|
|
||||||
|
@ -249,7 +272,7 @@
|
||||||
if (res2.ok) {
|
if (res2.ok) {
|
||||||
let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
|
let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
|
||||||
console.log(newData);
|
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);
|
console.log(newImage);
|
||||||
images = [...images, newImage];
|
images = [...images, newImage];
|
||||||
adventure.images = images;
|
adventure.images = images;
|
||||||
|
@ -1038,6 +1061,21 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
{#if !image.is_primary}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1 left-1 btn btn-success btn-xs z-10"
|
||||||
|
on:click={() => makePrimaryImage(image.id)}
|
||||||
|
>
|
||||||
|
<Star class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<!-- crown icon -->
|
||||||
|
|
||||||
|
<div class="absolute top-1 left-1 bg-warning text-white rounded-full p-1 z-10">
|
||||||
|
<Crown class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<img
|
<img
|
||||||
src={image.image}
|
src={image.image}
|
||||||
alt={image.id}
|
alt={image.id}
|
||||||
|
|
|
@ -9,7 +9,11 @@
|
||||||
let image_url: string | null = null;
|
let image_url: string | null = null;
|
||||||
|
|
||||||
$: adventure_images = adventures.flatMap((adventure) =>
|
$: adventure_images = adventures.flatMap((adventure) =>
|
||||||
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) {
|
function changeSlide(direction: string) {
|
||||||
if (direction === 'next' && currentSlide < adventure_images.length - 1) {
|
if (direction === 'next' && currentSlide < adventure_images.length - 1) {
|
||||||
currentSlide = currentSlide + 1;
|
currentSlide = currentSlide + 1;
|
||||||
|
|
|
@ -24,6 +24,7 @@ export type Adventure = {
|
||||||
images: {
|
images: {
|
||||||
id: string;
|
id: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
is_primary: boolean;
|
||||||
}[];
|
}[];
|
||||||
visits: {
|
visits: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -34,6 +34,16 @@
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (data.props.adventure) {
|
if (data.props.adventure) {
|
||||||
adventure = 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 {
|
} else {
|
||||||
notFound = true;
|
notFound = true;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue