diff --git a/src/actions/artworks/getArtworkColors.ts b/src/actions/artworks/getArtworkColors.ts new file mode 100644 index 0000000..d7d73fa --- /dev/null +++ b/src/actions/artworks/getArtworkColors.ts @@ -0,0 +1,11 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; + +export async function getArtworkColors(artworkId: string) { + return prisma.artworkColor.findMany({ + where: { artworkId }, + include: { color: true }, + orderBy: [{ type: "asc" }], + }); +} \ No newline at end of file diff --git a/src/app/artworks/[id]/page.tsx b/src/app/artworks/[id]/page.tsx index d87d725..df484ed 100644 --- a/src/app/artworks/[id]/page.tsx +++ b/src/app/artworks/[id]/page.tsx @@ -44,7 +44,7 @@ export default async function ArtworkSinglePage({ params }: { params: { id: stri
- {item && } + {item && }
diff --git a/src/components/artworks/ArtworkColorProcessor.tsx b/src/components/artworks/ArtworkColorProcessor.tsx index 5671796..494a284 100644 --- a/src/components/artworks/ArtworkColorProcessor.tsx +++ b/src/components/artworks/ArtworkColorProcessor.tsx @@ -38,7 +38,7 @@ export function ArtworkColorProcessor() { }; const done = - stats && + !!stats && stats.pending === 0 && stats.processing === 0 && stats.failed === 0 && diff --git a/src/components/artworks/single/ArtworkColors.tsx b/src/components/artworks/single/ArtworkColors.tsx index 6769356..17c7761 100644 --- a/src/components/artworks/single/ArtworkColors.tsx +++ b/src/components/artworks/single/ArtworkColors.tsx @@ -1,50 +1,62 @@ -"use client" +"use client"; -// import { generateArtworkColors } from "@/actions/artworks/generateArtworkColors"; -// import { generateImageColors } from "@/actions/portfolio/images/generateImageColors"; -import { ArtworkColor, Color } from "@/generated/prisma/client"; -// import { Color, ImageColor } from "@/generated/prisma"; -import { useState, useTransition } from "react"; +import { generateArtworkColorsForArtwork } from "@/actions/artworks/generateArtworkColors"; +import { getArtworkColors } from "@/actions/artworks/getArtworkColors"; +// import { generateArtworkColorsForArtwork } from "@/actions/colors/generateArtworkColorsForArtwork"; +// import { getArtworkColors } from "@/actions/colors/getArtworkColors"; +import { Button } from "@/components/ui/button"; +import type { ArtworkColor, Color } from "@/generated/prisma/client"; +import * as React from "react"; +// import { toast } from "sonner"; // if you use it -type ColorWithItems = ArtworkColor & { - color: Color -}; +type ColorWithItems = ArtworkColor & { color: Color }; -export default function ArtworkColors({ colors: initialColors, artworkId, fileKey, fileType }: { colors: ColorWithItems[], artworkId: string, fileKey: string, fileType?: string }) { - const [colors, setColors] = useState(initialColors); - const [isPending, startTransition] = useTransition(); +export default function ArtworkColors({ + colors: initialColors, + artworkId, +}: { + colors: ColorWithItems[]; + artworkId: string; +}) { + const [colors, setColors] = React.useState(initialColors); + const [isPending, startTransition] = React.useTransition(); - // const handleGenerate = () => { - // startTransition(async () => { - // try { - // const newColors = await generateArtworkColorsForArtwork(artworkId, fileKey, fileType); - // setColors(newColors); - // toast.success("Colors extracted successfully"); - // } catch (err) { - // toast.error("Failed to extract colors"); - // console.error(err); - // } - // }); - // }; + const handleGenerate = () => { + startTransition(async () => { + try { + const res = await generateArtworkColorsForArtwork(artworkId); + if (!res.ok) throw new Error(res.error ?? "Color generation failed"); + + const fresh = await getArtworkColors(artworkId); + setColors(fresh); + + // toast.success("Colors extracted successfully"); + } catch (err) { + // toast.error(err instanceof Error ? err.message : "Failed to extract colors"); + console.error(err); + } + }); + }; return ( <>

Artwork Colors

- {/* */} -
+ + +
{colors.map((item) => (
+ title={`${item.type} – ${item.color?.hex ?? ""}`} + /> ))}
); -} \ No newline at end of file +} diff --git a/src/components/artworks/single/ArtworkVariants.tsx b/src/components/artworks/single/ArtworkVariants.tsx index ba164d8..90fd3d2 100644 --- a/src/components/artworks/single/ArtworkVariants.tsx +++ b/src/components/artworks/single/ArtworkVariants.tsx @@ -1,15 +1,29 @@ -// import { ImageVariant } from "@/generated/prisma"; -// import { formatFileSize } from "@/utils/formatFileSize"; import { FileVariant } from "@/generated/prisma/client"; import { formatFileSize } from "@/utils/formatFileSize"; import NextImage from "next/image"; +const ORDER: Record = { + thumbnail: 0, + resized: 1, + modified: 2, + original: 3, +}; + +function byVariantOrder(a: FileVariant, b: FileVariant) { + const ra = ORDER[a.type] ?? 999; + const rb = ORDER[b.type] ?? 999; + if (ra !== rb) return ra - rb; + return a.type.localeCompare(b.type); +} + export default function ArtworkVariants({ variants }: { variants: FileVariant[] }) { + const sorted = [...variants].sort(byVariantOrder); + return ( <>

Variants

- {variants.map((variant) => ( + {sorted.map((variant) => (
{variant.type} | {variant.width}x{variant.height}px | {variant.sizeBytes ? formatFileSize(variant.sizeBytes) : "-"}
{variant.s3Key && ( diff --git a/src/components/uploads/UploadImageForm.tsx b/src/components/uploads/UploadImageForm.tsx index 5315cf1..f4b7aef 100644 --- a/src/components/uploads/UploadImageForm.tsx +++ b/src/components/uploads/UploadImageForm.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { createImage } from "@/actions/uploads/createImage"; import { Button } from "@/components/ui/button"; @@ -8,46 +8,117 @@ import { fileUploadSchema } from "@/schemas/artworks/imageSchema"; import { zodResolver } from "@hookform/resolvers/zod"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import * as React from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import * as z from "zod/v4"; +type UploadStatus = "empty" | "queued" | "uploading" | "done" | "error"; + export default function UploadImageForm() { const router = useRouter(); - const [preview, setPreview] = useState(null); const form = useForm>({ resolver: zodResolver(fileUploadSchema), - defaultValues: { - file: undefined - }, - }) + defaultValues: { file: undefined }, + }); + + const [status, setStatus] = React.useState("empty"); + const [progress, setProgress] = React.useState(0); + const [error, setError] = React.useState(null); + + const [file, setFile] = React.useState(null); + const [previewUrl, setPreviewUrl] = React.useState(null); + + const intervalRef = React.useRef(null); + + React.useEffect(() => { + return () => { + if (previewUrl) URL.revokeObjectURL(previewUrl); + if (intervalRef.current) window.clearInterval(intervalRef.current); + }; + }, [previewUrl]); + + const startSimProgress = () => { + if (intervalRef.current) window.clearInterval(intervalRef.current); + intervalRef.current = window.setInterval(() => { + setProgress((p) => Math.min(90, p + Math.random() * 7 + 1)); + }, 250); + }; + + const stopSimProgress = () => { + if (intervalRef.current) window.clearInterval(intervalRef.current); + intervalRef.current = null; + }; + + const resetAll = () => { + stopSimProgress(); + setStatus("empty"); + setProgress(0); + setError(null); + setFile(null); + + if (previewUrl) URL.revokeObjectURL(previewUrl); + setPreviewUrl(null); + + form.reset(); + }; const onFileChange = (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; - const file = files[0]; - const reader = new FileReader(); + const f = files[0]; + setFile(f); + setError(null); + setStatus("queued"); + setProgress(0); - reader.onloadend = () => { - if (typeof reader.result === 'string') { - setPreview(reader.result as string); - } - }; - reader.readAsDataURL(file); - form.setValue("file", files); + if (previewUrl) URL.revokeObjectURL(previewUrl); + setPreviewUrl(URL.createObjectURL(f)); + + // keep schema shape (FileList) + form.setValue("file", files, { shouldValidate: true }); }; async function onSubmit(values: z.infer) { - const image = await createImage(values) - if (image) { - toast.success("Image created") - router.push(`/artworks/${image.id}`) + setError(null); + + const hasFile = (values.file?.length ?? 0) > 0; + if (!hasFile) return; + + setStatus("uploading"); + setProgress((p) => Math.max(p, 5)); + startSimProgress(); + + try { + const image = await createImage(values); + + stopSimProgress(); + setProgress(100); + + if (image) { + setStatus("done"); + toast.success("Image created"); + router.push(`/artworks/${image.id}`); + return; + } + + setStatus("error"); + setError("Upload failed"); + toast.error("Upload failed"); + } catch (err) { + stopSimProgress(); + setProgress(100); + setStatus("error"); + const msg = err instanceof Error ? err.message : "Upload failed"; + setError(msg); + toast.error(msg); } } + const isBusy = status === "uploading"; + return ( <>
@@ -59,34 +130,61 @@ export default function UploadImageForm() { Choose image to upload - onFileChange(e)} - /> + )} /> +
- - + + + + +
-
- { - preview ? - Preview - : - null - } -
+ + {file ? ( +
+
+
+ {previewUrl ? ( + {file.name} + ) : null} +
+ +
+
+
{file.name}
+
+ {status === "queued" && "Queued"} + {status === "uploading" && "Uploading / Processing"} + {status === "done" && "Done"} + {status === "error" && "Error"} +
+
+
+ +
+
+
+
+ + {status === "error" && error ? ( +
{error}
+ ) : null} +
+
+
+ ) : null} ); -} \ No newline at end of file +}