diff --git a/prisma/migrations/20250711232415_image_done_check/migration.sql b/prisma/migrations/20250711232415_image_done_check/migration.sql
new file mode 100644
index 0000000..0d082c1
--- /dev/null
+++ b/prisma/migrations/20250711232415_image_done_check/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Image" ADD COLUMN "isDone" BOOLEAN DEFAULT false;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 7f4e654..b6fe0c2 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -133,6 +133,7 @@ model Image {
creationYear Int?
fileSize Int?
creationDate DateTime?
+ isDone Boolean? @default(false)
albumId String?
artistId String?
diff --git a/src/actions/images/updateImage.ts b/src/actions/images/updateImage.ts
index a3057b2..021f73b 100644
--- a/src/actions/images/updateImage.ts
+++ b/src/actions/images/updateImage.ts
@@ -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
}
});
diff --git a/src/actions/images/updateImageSortOrder.ts b/src/actions/images/updateImageSortOrder.ts
new file mode 100644
index 0000000..a389926
--- /dev/null
+++ b/src/actions/images/updateImageSortOrder.ts
@@ -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 },
+ })
+ )
+ );
+}
diff --git a/src/app/images/page.tsx b/src/app/images/page.tsx
index c081641..faa5010 100644
--- a/src/app/images/page.tsx
+++ b/src/app/images/page.tsx
@@ -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 (
diff --git a/src/components/images/edit/EditImageForm.tsx b/src/components/images/edit/EditImageForm.tsx
index d6bcf00..f9f7e76 100644
--- a/src/components/images/edit/EditImageForm.tsx
+++ b/src/components/images/edit/EditImageForm.tsx
@@ -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
}}
/>
+
(
+
+
+ Image Done
+ Is all image data correct and image is ready to be published?
+
+
+
+
+
+ )}
+ />
+
diff --git a/src/components/images/list/ListImages.tsx b/src/components/images/list/ListImages.tsx
index d3f3fa8..73aaab9 100644
--- a/src/components/images/list/ListImages.tsx
+++ b/src/components/images/list/ListImages.tsx
@@ -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 (
-
- {images.map((image) => (
-
-
-
-
- {image.imageName}
-
-
-
-
-
-
-
- ))}
-
+
{
+ const image = images.find(g => g.id === item.id)!;
+ return (
+
+ );
+ }}
+ />
);
}
\ No newline at end of file
diff --git a/src/components/sort/SortableImage.tsx b/src/components/sort/SortableImage.tsx
new file mode 100644
index 0000000..623be35
--- /dev/null
+++ b/src/components/sort/SortableImage.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+ {/* Content */}
+
+
{item.name}
+
+ {item.album && (
+
{item.album}
+ )}
+ {item.gallery && (
+
{item.gallery}
+ )}
+ {dateDisplay && (
+
{dateDisplay}
+ )}
+
+
+ {/* Status icon in bottom-left corner */}
+
+ {item.isDone ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/schemas/images/imageSchema.ts b/src/schemas/images/imageSchema.ts
index 07d62d0..a1d0f53 100644
--- a/src/schemas/images/imageSchema.ts
+++ b/src/schemas/images/imageSchema.ts
@@ -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(),