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?
|
creationYear Int?
|
||||||
fileSize Int?
|
fileSize Int?
|
||||||
creationDate DateTime?
|
creationDate DateTime?
|
||||||
|
isDone Boolean? @default(false)
|
||||||
|
|
||||||
albumId String?
|
albumId String?
|
||||||
artistId String?
|
artistId String?
|
||||||
|
@ -29,7 +29,8 @@ export async function updateImage(
|
|||||||
albumId,
|
albumId,
|
||||||
artistId,
|
artistId,
|
||||||
tagIds,
|
tagIds,
|
||||||
categoryIds
|
categoryIds,
|
||||||
|
isDone
|
||||||
} = validated.data;
|
} = validated.data;
|
||||||
|
|
||||||
const updatedImage = await prisma.image.update({
|
const updatedImage = await prisma.image.update({
|
||||||
@ -49,6 +50,7 @@ export async function updateImage(
|
|||||||
creationDate,
|
creationDate,
|
||||||
albumId,
|
albumId,
|
||||||
artistId,
|
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";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function ImagesPage() {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -80,6 +80,7 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
|||||||
creationYear: image.creationYear || undefined,
|
creationYear: image.creationYear || undefined,
|
||||||
fileSize: image.fileSize || undefined,
|
fileSize: image.fileSize || undefined,
|
||||||
creationDate: image.creationDate ? new Date(image.creationDate) : undefined,
|
creationDate: image.creationDate ? new Date(image.creationDate) : undefined,
|
||||||
|
isDone: image.isDone || false,
|
||||||
|
|
||||||
artistId: image.artist?.id || undefined,
|
artistId: image.artist?.id || undefined,
|
||||||
albumId: image.album?.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">
|
<div className="flex flex-col gap-4">
|
||||||
<Button type="submit">Submit</Button>
|
<Button type="submit">Submit</Button>
|
||||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</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 { updateImageSortOrder } from "@/actions/images/updateImageSortOrder";
|
||||||
import { Image } from "@/generated/prisma";
|
import { SortableImage } from "@/components/sort/SortableImage";
|
||||||
import NetImage from "next/image";
|
import { SortableList } from "@/components/sort/SortableList";
|
||||||
import Link from "next/link";
|
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 (
|
return (
|
||||||
<div className="columns-1 sm:columns-2 md:columns-3 xl:columns-4 gap-4 space-y-4">
|
<SortableList
|
||||||
{images.map((image) => (
|
items={sortableItems}
|
||||||
<div key={image.id} className="break-inside-avoid">
|
onReorder={handleReorder}
|
||||||
<Link href={`/images/edit/${image.id}`} key={image.id}>
|
onSortDefault={handleSortDefault}
|
||||||
<Card className="overflow-hidden">
|
defaultSortLabel="Sort by name"
|
||||||
<CardHeader>
|
renderItem={(item) => {
|
||||||
<CardTitle className="text-base truncate">{image.imageName}</CardTitle>
|
const image = images.find(g => g.id === item.id)!;
|
||||||
</CardHeader>
|
return (
|
||||||
<CardContent className="flex justify-center items-center">
|
<SortableImage
|
||||||
<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" />
|
id={image.id}
|
||||||
</CardContent>
|
item={{
|
||||||
</Card>
|
id: image.id,
|
||||||
</Link>
|
name: image.imageName,
|
||||||
</div>
|
fileKey: image.fileKey,
|
||||||
))}
|
altText: image.altText || image.imageName,
|
||||||
</div>
|
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(),
|
creationYear: z.number().min(1900).max(2100).optional(),
|
||||||
fileSize: z.number().optional(),
|
fileSize: z.number().optional(),
|
||||||
creationDate: z.date().optional(),
|
creationDate: z.date().optional(),
|
||||||
|
isDone: z.boolean().optional(),
|
||||||
|
|
||||||
albumId: z.string().optional(),
|
albumId: z.string().optional(),
|
||||||
artistId: z.string().optional(),
|
artistId: z.string().optional(),
|
||||||
|
Reference in New Issue
Block a user