Add new booelan and stuff

This commit is contained in:
2025-07-12 13:17:57 +02:00
parent 7cb9fa6320
commit a87ac03c00
9 changed files with 224 additions and 24 deletions

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Image" ADD COLUMN "isDone" BOOLEAN DEFAULT false;

View File

@ -133,6 +133,7 @@ model Image {
creationYear Int?
fileSize Int?
creationDate DateTime?
isDone Boolean? @default(false)
albumId String?
artistId String?

View File

@ -29,7 +29,8 @@ export async function updateImage(
albumId,
artistId,
tagIds,
categoryIds
categoryIds,
isDone
} = validated.data;
const updatedImage = await prisma.image.update({
@ -49,6 +50,7 @@ export async function updateImage(
creationDate,
albumId,
artistId,
isDone
}
});

View File

@ -0,0 +1,15 @@
'use server';
import prisma from "@/lib/prisma";
import { SortableItem } from "@/types/SortableItem";
export async function updateImageSortOrder(items: SortableItem[]) {
await Promise.all(
items.map(item =>
prisma.image.update({
where: { id: item.id },
data: { sortIndex: item.sortIndex },
})
)
);
}

View File

@ -4,7 +4,18 @@ import { PlusCircleIcon } from "lucide-react";
import Link from "next/link";
export default async function ImagesPage() {
const images = await prisma.image.findMany({ orderBy: { imageName: "asc" } });
const images = await prisma.image.findMany(
{
orderBy: [
{ album: { sortIndex: 'asc' } },
{ sortIndex: 'asc' },
{ creationDate: 'desc' },
{ imageName: 'asc' }],
include: {
album: { include: { gallery: true } }
}
}
);
return (
<div>

View File

@ -80,6 +80,7 @@ export default function EditImageForm({ image, albums, artists, categories, tags
creationYear: image.creationYear || undefined,
fileSize: image.fileSize || undefined,
creationDate: image.creationDate ? new Date(image.creationDate) : undefined,
isDone: image.isDone || false,
artistId: image.artist?.id || undefined,
albumId: image.album?.id || undefined,
@ -477,6 +478,22 @@ export default function EditImageForm({ image, albums, artists, categories, tags
}}
/>
<FormField
control={form.control}
name="isDone"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Image Done</FormLabel>
<FormDescription>Is all image data correct and image is ready to be published?</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>

View File

@ -1,27 +1,70 @@
// "use client"
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Image } from "@/generated/prisma";
import NetImage from "next/image";
import Link from "next/link";
import { updateImageSortOrder } from "@/actions/images/updateImageSortOrder";
import { SortableImage } from "@/components/sort/SortableImage";
import { SortableList } from "@/components/sort/SortableList";
import { Album, Gallery, Image } from "@/generated/prisma";
import { SortableItem } from "@/types/SortableItem";
import { useEffect, useState } from "react";
type ImagesWithItems = Image & {
album: Album & {
gallery: Gallery | null
} | null
};
export default function ListImages({ images }: { images: ImagesWithItems[] }) {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
const sortableItems: SortableItem[] = images.map(image => ({
id: image.id,
sortIndex: image.sortIndex,
label: image.imageName,
}));
const handleSortDefault = async () => {
const sorted = [...sortableItems]
.sort((a, b) => a.label.localeCompare(b.label))
.map((item, index) => ({ ...item, sortIndex: index * 10 }));
await updateImageSortOrder(sorted);
};
const handleReorder = async (items: SortableItem[]) => {
await updateImageSortOrder(items);
};
if (!isMounted) return null;
export default function ListImages({ images }: { images: Image[] }) {
return (
<div className="columns-1 sm:columns-2 md:columns-3 xl:columns-4 gap-4 space-y-4">
{images.map((image) => (
<div key={image.id} className="break-inside-avoid">
<Link href={`/images/edit/${image.id}`} key={image.id}>
<Card className="overflow-hidden">
<CardHeader>
<CardTitle className="text-base truncate">{image.imageName}</CardTitle>
</CardHeader>
<CardContent className="flex justify-center items-center">
<NetImage src={`/api/image/thumbnails/${image.fileKey}.webp`} alt={image.altText ? image.altText : "Image"} width={200} height={200} className="rounded max-w-full h-auto object-contain" />
</CardContent>
</Card>
</Link>
</div>
))}
</div>
<SortableList
items={sortableItems}
onReorder={handleReorder}
onSortDefault={handleSortDefault}
defaultSortLabel="Sort by name"
renderItem={(item) => {
const image = images.find(g => g.id === item.id)!;
return (
<SortableImage
id={image.id}
item={{
id: image.id,
name: image.imageName,
fileKey: image.fileKey,
altText: image.altText || image.imageName,
album: image.album?.name || undefined,
gallery: image.album?.gallery?.name || undefined,
isDone: image.isDone || false,
creationDate: image.creationDate || undefined,
creationMonth: image.creationMonth || undefined,
creationYear: image.creationYear || undefined
}}
/>
);
}}
/>
);
}

View File

@ -0,0 +1,108 @@
'use client';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import clsx from 'clsx';
import { CheckCircle, Circle, GripVertical } from 'lucide-react';
import NextImage from 'next/image';
import Link from 'next/link';
type SortableCardItemProps = {
id: string;
item: {
id: string;
name: string;
fileKey: string;
altText: string;
album?: string;
gallery?: string;
isDone?: boolean;
creationDate?: Date | string;
creationMonth?: number;
creationYear?: number;
};
};
export function SortableImage({ id, item }: SortableCardItemProps) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const href = `/images/edit/${item.id}`;
let dateDisplay = null;
if (item.creationDate instanceof Date) {
dateDisplay = item.creationDate.toLocaleDateString('de-DE');
} else if (typeof item.creationDate === 'string') {
const parsed = new Date(item.creationDate);
if (!isNaN(parsed.getTime())) {
dateDisplay = parsed.toLocaleDateString('de-DE');
}
} else if (
typeof item.creationMonth === 'number' &&
typeof item.creationYear === 'number'
) {
const date = new Date(item.creationYear, item.creationMonth);
dateDisplay = date.toLocaleDateString('en-US', {
month: 'long',
year: 'numeric',
});
}
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
className="relative cursor-grab active:cursor-grabbing"
>
<div
{...listeners}
className="absolute top-2 left-2 z-20 text-muted-foreground bg-white/70 rounded-full p-1"
title="Drag to reorder"
>
<GripVertical className="w-4 h-4" />
</div>
<Link href={href}>
<div className="group rounded-lg border overflow-hidden hover:shadow-md transition-shadow bg-background relative">
<div className="relative aspect-[4/3] w-full bg-muted items-center justify-center">
<NextImage
src={`/api/image/thumbnails/${item.fileKey}.webp`}
alt={item.altText ? item.altText : "Image"}
fill
className={clsx("object-cover transition duration-300")}
/>
</div>
{/* Content */}
<div className="p-4 text-center">
<h2 className="text-lg font-semibold truncate">{item.name}</h2>
{item.album && (
<p className="text-sm text-muted-foreground mt-1">{item.album}</p>
)}
{item.gallery && (
<p className="text-sm text-muted-foreground mt-1">{item.gallery}</p>
)}
{dateDisplay && (
<p className="text-xs text-muted-foreground mt-1">{dateDisplay}</p>
)}
</div>
{/* Status icon in bottom-left corner */}
<div className="absolute bottom-2 left-2 z-10 bg-white/80 rounded-full p-1">
{item.isDone ? (
<CheckCircle className="w-4 h-4 text-green-600" />
) : (
<Circle className="w-4 h-4 text-muted-foreground" />
)}
</div>
</div>
</Link>
</div>
);
}

View File

@ -25,6 +25,7 @@ export const imageSchema = z.object({
creationYear: z.number().min(1900).max(2100).optional(),
fileSize: z.number().optional(),
creationDate: z.date().optional(),
isDone: z.boolean().optional(),
albumId: z.string().optional(),
artistId: z.string().optional(),