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 (
<>
-
- {
- preview ?
-
- :
- null
- }
-
+
+ {file ? (
+
+
+
+ {previewUrl ? (
+
+ ) : 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
+}