Add new booelan and stuff
This commit is contained in:
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "isDone" BOOLEAN DEFAULT false;
|
@ -133,6 +133,7 @@ model Image {
|
||||
creationYear Int?
|
||||
fileSize Int?
|
||||
creationDate DateTime?
|
||||
isDone Boolean? @default(false)
|
||||
|
||||
albumId String?
|
||||
artistId String?
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
15
src/actions/images/updateImageSortOrder.ts
Normal file
15
src/actions/images/updateImageSortOrder.ts
Normal 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 },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
108
src/components/sort/SortableImage.tsx
Normal file
108
src/components/sort/SortableImage.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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(),
|
||||
|
Reference in New Issue
Block a user