Refactor colors and palettes
This commit is contained in:
@ -0,0 +1,172 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `type` on the `ColorPalette` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `blue` on the `ImageColor` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `green` on the `ImageColor` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `hex` on the `ImageColor` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `name` on the `ImageColor` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `red` on the `ImageColor` table. All the data in the column will be lost.
|
||||
- You are about to drop the `PixelSummary` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `ThemeSeed` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `_ImagePalettes` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `_ImageToExtractColor` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `_ImageToImageColor` table. If the table is not empty, all the data it contains will be lost.
|
||||
- A unique constraint covering the columns `[galleryId,slug]` on the table `Album` will be added. If there are existing duplicate values, this will fail.
|
||||
- A unique constraint covering the columns `[imageId,type]` on the table `ImageColor` will be added. If there are existing duplicate values, this will fail.
|
||||
- A unique constraint covering the columns `[imageId]` on the table `ImageMetadata` will be added. If there are existing duplicate values, this will fail.
|
||||
- A unique constraint covering the columns `[imageId]` on the table `ImageStats` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `colorId` to the `ImageColor` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `imageId` to the `ImageColor` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "PixelSummary" DROP CONSTRAINT "PixelSummary_imageId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ThemeSeed" DROP CONSTRAINT "ThemeSeed_imageId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_ImagePalettes" DROP CONSTRAINT "_ImagePalettes_A_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_ImagePalettes" DROP CONSTRAINT "_ImagePalettes_B_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_ImageToExtractColor" DROP CONSTRAINT "_ImageToExtractColor_A_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_ImageToExtractColor" DROP CONSTRAINT "_ImageToExtractColor_B_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_ImageToImageColor" DROP CONSTRAINT "_ImageToImageColor_A_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_ImageToImageColor" DROP CONSTRAINT "_ImageToImageColor_B_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "ImageColor_name_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Album" ADD COLUMN "coverImageId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ColorPalette" DROP COLUMN "type";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Gallery" ADD COLUMN "coverImageId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "source" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ImageColor" DROP COLUMN "blue",
|
||||
DROP COLUMN "green",
|
||||
DROP COLUMN "hex",
|
||||
DROP COLUMN "name",
|
||||
DROP COLUMN "red",
|
||||
ADD COLUMN "colorId" TEXT NOT NULL,
|
||||
ADD COLUMN "imageId" TEXT NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "PixelSummary";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "ThemeSeed";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "_ImagePalettes";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "_ImageToExtractColor";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "_ImageToImageColor";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Color" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"hex" TEXT,
|
||||
"blue" INTEGER,
|
||||
"green" INTEGER,
|
||||
"red" INTEGER,
|
||||
|
||||
CONSTRAINT "Color_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ImagePalette" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"imageId" TEXT NOT NULL,
|
||||
"paletteId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "ImagePalette_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ImageExtractColor" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"imageId" TEXT NOT NULL,
|
||||
"extractId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"colorId" TEXT,
|
||||
|
||||
CONSTRAINT "ImageExtractColor_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Color_name_key" ON "Color"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ImagePalette_imageId_type_key" ON "ImagePalette"("imageId", "type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ImageExtractColor_imageId_type_key" ON "ImageExtractColor"("imageId", "type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Album_galleryId_slug_key" ON "Album"("galleryId", "slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ImageColor_imageId_type_key" ON "ImageColor"("imageId", "type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ImageMetadata_imageId_key" ON "ImageMetadata"("imageId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ImageStats_imageId_key" ON "ImageStats"("imageId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Gallery" ADD CONSTRAINT "Gallery_coverImageId_fkey" FOREIGN KEY ("coverImageId") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Album" ADD CONSTRAINT "Album_coverImageId_fkey" FOREIGN KEY ("coverImageId") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ImagePalette" ADD CONSTRAINT "ImagePalette_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ImagePalette" ADD CONSTRAINT "ImagePalette_paletteId_fkey" FOREIGN KEY ("paletteId") REFERENCES "ColorPalette"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ImageExtractColor" ADD CONSTRAINT "ImageExtractColor_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ImageExtractColor" ADD CONSTRAINT "ImageExtractColor_extractId_fkey" FOREIGN KEY ("extractId") REFERENCES "ExtractColor"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ImageExtractColor" ADD CONSTRAINT "ImageExtractColor_colorId_fkey" FOREIGN KEY ("colorId") REFERENCES "Color"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ImageColor" ADD CONSTRAINT "ImageColor_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ImageColor" ADD CONSTRAINT "ImageColor_colorId_fkey" FOREIGN KEY ("colorId") REFERENCES "Color"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `colorId` on the `ImageExtractColor` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ImageExtractColor" DROP CONSTRAINT "ImageExtractColor_colorId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ImageExtractColor" DROP COLUMN "colorId";
|
@ -24,8 +24,8 @@ model Gallery {
|
||||
|
||||
description String?
|
||||
|
||||
// coverImageId String?
|
||||
// coverImage Image? @relation("GalleryCoverImage", fields: [coverImageId], references: [id])
|
||||
coverImageId String?
|
||||
coverImage Image? @relation("GalleryCoverImage", fields: [coverImageId], references: [id])
|
||||
|
||||
albums Album[]
|
||||
}
|
||||
@ -35,17 +35,19 @@ model Album {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String
|
||||
slug String
|
||||
name String
|
||||
|
||||
description String?
|
||||
|
||||
// coverImageId String?
|
||||
coverImageId String?
|
||||
galleryId String?
|
||||
// coverImage Image? @relation("AlbumCoverImage", fields: [coverImageId], references: [id])
|
||||
coverImage Image? @relation("AlbumCoverImage", fields: [coverImageId], references: [id])
|
||||
gallery Gallery? @relation(fields: [galleryId], references: [id])
|
||||
|
||||
images Image[]
|
||||
|
||||
@@unique([galleryId, slug])
|
||||
}
|
||||
|
||||
model Artist {
|
||||
@ -77,6 +79,30 @@ model Social {
|
||||
artist Artist? @relation(fields: [artistId], references: [id])
|
||||
}
|
||||
|
||||
model Category {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String @unique
|
||||
|
||||
description String?
|
||||
|
||||
images Image[] @relation("ImageCategories")
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String @unique
|
||||
|
||||
description String?
|
||||
|
||||
images Image[] @relation("ImageTags")
|
||||
}
|
||||
|
||||
model Image {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
@ -91,6 +117,7 @@ model Image {
|
||||
description String?
|
||||
fileType String?
|
||||
imageData String?
|
||||
source String?
|
||||
creationMonth Int?
|
||||
creationYear Int?
|
||||
fileSize Int?
|
||||
@ -100,22 +127,22 @@ model Image {
|
||||
artistId String?
|
||||
album Album? @relation(fields: [albumId], references: [id])
|
||||
artist Artist? @relation(fields: [artistId], references: [id])
|
||||
// sourceId String?
|
||||
// source Source? @relation(fields: [sourceId], references: [id])
|
||||
|
||||
metadata ImageMetadata[]
|
||||
pixels PixelSummary[]
|
||||
stats ImageStats[]
|
||||
theme ThemeSeed[]
|
||||
metadata ImageMetadata?
|
||||
stats ImageStats?
|
||||
|
||||
colors ImageColor[]
|
||||
extractColors ImageExtractColor[]
|
||||
palettes ImagePalette[]
|
||||
variants ImageVariant[]
|
||||
|
||||
// albumCover Album[] @relation("AlbumCoverImage")
|
||||
// galleryCover Gallery[] @relation("GalleryCoverImage")
|
||||
// pixels PixelSummary[]
|
||||
// theme ThemeSeed[]
|
||||
albumCover Album[] @relation("AlbumCoverImage")
|
||||
galleryCover Gallery[] @relation("GalleryCoverImage")
|
||||
categories Category[] @relation("ImageCategories")
|
||||
colors ImageColor[] @relation("ImageToImageColor")
|
||||
extractColors ExtractColor[] @relation("ImageToExtractColor")
|
||||
palettes ColorPalette[] @relation("ImagePalettes")
|
||||
// colors ImageColor[] @relation("ImageToImageColor")
|
||||
tags Tag[] @relation("ImageTags")
|
||||
// palettes ColorPalette[] @relation("ImagePalettes")
|
||||
}
|
||||
|
||||
model ImageMetadata {
|
||||
@ -123,7 +150,7 @@ model ImageMetadata {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
imageId String @unique
|
||||
depth String
|
||||
format String
|
||||
space String
|
||||
@ -148,7 +175,7 @@ model ImageStats {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
imageId String @unique
|
||||
entropy Float
|
||||
sharpness Float
|
||||
dominantB Int
|
||||
@ -184,10 +211,10 @@ model ColorPalette {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String
|
||||
type String
|
||||
|
||||
items ColorPaletteItem[]
|
||||
images Image[] @relation("ImagePalettes")
|
||||
images ImagePalette[]
|
||||
// images Image[] @relation("ImagePalettes")
|
||||
}
|
||||
|
||||
model ColorPaletteItem {
|
||||
@ -217,10 +244,11 @@ model ExtractColor {
|
||||
hue Float?
|
||||
saturation Float?
|
||||
|
||||
images Image[] @relation("ImageToExtractColor")
|
||||
// images Image[] @relation("ImageToExtractColor")
|
||||
images ImageExtractColor[]
|
||||
}
|
||||
|
||||
model ImageColor {
|
||||
model Color {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@ -233,53 +261,74 @@ model ImageColor {
|
||||
green Int?
|
||||
red Int?
|
||||
|
||||
images Image[] @relation("ImageToImageColor")
|
||||
images ImageColor[]
|
||||
}
|
||||
|
||||
model ThemeSeed {
|
||||
// model ThemeSeed {
|
||||
// id String @id @default(cuid())
|
||||
// createdAt DateTime @default(now())
|
||||
// updatedAt DateTime @updatedAt
|
||||
|
||||
// imageId String
|
||||
// seedHex String
|
||||
|
||||
// image Image @relation(fields: [imageId], references: [id])
|
||||
// }
|
||||
|
||||
// model PixelSummary {
|
||||
// id String @id @default(cuid())
|
||||
// createdAt DateTime @default(now())
|
||||
// updatedAt DateTime @updatedAt
|
||||
|
||||
// imageId String
|
||||
// channels Int
|
||||
// height Int
|
||||
// width Int
|
||||
|
||||
// image Image @relation(fields: [imageId], references: [id])
|
||||
// }
|
||||
|
||||
model ImagePalette {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
seedHex String
|
||||
paletteId String
|
||||
type String
|
||||
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
palette ColorPalette @relation(fields: [paletteId], references: [id])
|
||||
|
||||
@@unique([imageId, type])
|
||||
}
|
||||
|
||||
model PixelSummary {
|
||||
model ImageExtractColor {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
channels Int
|
||||
height Int
|
||||
width Int
|
||||
extractId String
|
||||
type String
|
||||
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
extract ExtractColor @relation(fields: [extractId], references: [id])
|
||||
|
||||
@@unique([imageId, type])
|
||||
}
|
||||
|
||||
model Category {
|
||||
model ImageColor {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String @unique
|
||||
imageId String
|
||||
colorId String
|
||||
type String
|
||||
|
||||
description String?
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
color Color @relation(fields: [colorId], references: [id])
|
||||
|
||||
images Image[] @relation("ImageCategories")
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String @unique
|
||||
|
||||
description String?
|
||||
|
||||
images Image[] @relation("ImageTags")
|
||||
@@unique([imageId, type])
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ export async function updateAlbum(
|
||||
name: values.name,
|
||||
slug: values.slug,
|
||||
description: values.description,
|
||||
galleryId: values.galleryId
|
||||
galleryId: values.galleryId,
|
||||
coverImageId: values.coverImageId
|
||||
}
|
||||
})
|
||||
}
|
@ -16,6 +16,7 @@ export async function updateGallery(
|
||||
name: values.name,
|
||||
slug: values.slug,
|
||||
description: values.description,
|
||||
coverImageId: values.coverImageId
|
||||
}
|
||||
})
|
||||
}
|
@ -16,15 +16,16 @@ export async function generateExtractColors(imageId: string, fileKey: string) {
|
||||
metadata: true
|
||||
}
|
||||
})
|
||||
const buffer = await getImageBufferFromS3(fileKey);
|
||||
if(!image) throw new Error("Image not found");
|
||||
|
||||
const buffer = await getImageBufferFromS3(fileKey);
|
||||
const format = image.metadata?.format || "jpeg";
|
||||
const imageDataUrl = `data:${image.fileType};base64,${buffer.toString("base64")}`;
|
||||
|
||||
const pixels = await new Promise<NdArray<Uint8Array>>((resolve, reject) => {
|
||||
getPixels(imageDataUrl, 'image/' + image.metadata[0].format || "image/jpeg", (err, pixels) => {
|
||||
getPixels(imageDataUrl, `image/${format}`, (err, result) => {
|
||||
if (err) reject(err);
|
||||
else resolve(pixels);
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
@ -34,14 +35,12 @@ export async function generateExtractColors(imageId: string, fileKey: string) {
|
||||
height: pixels.shape[1]
|
||||
});
|
||||
|
||||
let typeIndex = 0;
|
||||
|
||||
for (const c of extracted) {
|
||||
const name = generateExtractColorName(c.hex, c.hue, c.saturation, c.area);
|
||||
|
||||
await prisma.image.update({
|
||||
where: { id: imageId },
|
||||
data: {
|
||||
extractColors: {
|
||||
connectOrCreate: {
|
||||
const extract = await prisma.extractColor.upsert({
|
||||
where: { name },
|
||||
create: {
|
||||
name,
|
||||
@ -53,17 +52,29 @@ export async function generateExtractColors(imageId: string, fileKey: string) {
|
||||
saturation: c.saturation,
|
||||
area: c.area,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
await prisma.imageExtractColor.upsert({
|
||||
where: {
|
||||
imageId_type: {
|
||||
imageId,
|
||||
type: `color-${typeIndex}`,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
imageId,
|
||||
extractId: extract.id,
|
||||
type: `color-${typeIndex}`,
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
typeIndex++;
|
||||
}
|
||||
|
||||
return await prisma.extractColor.findMany({
|
||||
where: {
|
||||
images: {
|
||||
some: { id: imageId },
|
||||
},
|
||||
},
|
||||
return await prisma.imageExtractColor.findMany({
|
||||
where: { imageId },
|
||||
include: { extract: true },
|
||||
});
|
||||
}
|
@ -10,45 +10,57 @@ export async function generateImageColors(imageId: string, fileKey: string) {
|
||||
const buffer = await getImageBufferFromS3(fileKey);
|
||||
const palette = await Vibrant.from(buffer).getPalette();
|
||||
|
||||
const vibrantHexes = Object.fromEntries(
|
||||
Object.entries(palette).map(([key, swatch]) => {
|
||||
const vibrantHexes = Object.entries(palette).map(([key, swatch]) => {
|
||||
const castSwatch = swatch as VibrantSwatch | null;
|
||||
const rgb = castSwatch?._rgb;
|
||||
const hex = castSwatch?.hex || (rgb ? rgbToHex(rgb) : undefined);
|
||||
return [key, hex];
|
||||
})
|
||||
);
|
||||
return { type: key, hex };
|
||||
});
|
||||
|
||||
for (const [type, hex] of Object.entries(vibrantHexes)) {
|
||||
for (const { type, hex } of vibrantHexes) {
|
||||
if (!hex) continue;
|
||||
|
||||
const [r, g, b] = hex.match(/\w\w/g)!.map((h) => parseInt(h, 16));
|
||||
const name = generateColorName(hex);
|
||||
|
||||
await prisma.image.update({
|
||||
where: { id: imageId },
|
||||
data: {
|
||||
colors: {
|
||||
connectOrCreate: {
|
||||
where: { name: name },
|
||||
const color = await prisma.color.upsert({
|
||||
where: { name },
|
||||
create: {
|
||||
name: name,
|
||||
type: type,
|
||||
hex: hex,
|
||||
name,
|
||||
type,
|
||||
hex,
|
||||
red: r,
|
||||
green: g,
|
||||
blue: b,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
update: {
|
||||
hex,
|
||||
red: r,
|
||||
green: g,
|
||||
blue: b,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.imageColor.upsert({
|
||||
where: {
|
||||
imageId_type: {
|
||||
imageId,
|
||||
type,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
imageId,
|
||||
colorId: color.id,
|
||||
type,
|
||||
},
|
||||
update: {
|
||||
colorId: color.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.imageColor.findMany({
|
||||
where: {
|
||||
images: {
|
||||
some: { id: imageId },
|
||||
},
|
||||
},
|
||||
where: { imageId },
|
||||
include: { color: true },
|
||||
});
|
||||
}
|
@ -44,15 +44,16 @@ export async function generatePaletteAction(imageId: string, fileKey: string) {
|
||||
await upsertPalettes(neutralVariantTones, imageId, "neutralVariant");
|
||||
await upsertPalettes(errorTones, imageId, "error");
|
||||
|
||||
await prisma.themeSeed.create({
|
||||
data: {
|
||||
seedHex,
|
||||
imageId,
|
||||
return await prisma.imagePalette.findMany({
|
||||
where: {
|
||||
imageId: imageId
|
||||
},
|
||||
});
|
||||
|
||||
return await prisma.colorPalette.findMany({
|
||||
where: { images: { some: { id: imageId } } },
|
||||
include: { items: true },
|
||||
include: {
|
||||
palette: {
|
||||
include: {
|
||||
items: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
@ -7,6 +7,10 @@ export default async function AlbumsEditPage({ params }: { params: { id: string
|
||||
const album = await prisma.album.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
coverImage: true,
|
||||
images: true
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -7,7 +7,7 @@ export default async function AlbumsPage() {
|
||||
const albums = await prisma.album.findMany(
|
||||
{
|
||||
include: { gallery: true, images: { select: { id: true } } },
|
||||
orderBy: { createdAt: "asc" }
|
||||
orderBy: { name: "asc" }
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -4,7 +4,10 @@ import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function ArtistsPage() {
|
||||
const artists = await prisma.artist.findMany({ orderBy: { createdAt: "asc" } });
|
||||
const artists = await prisma.artist.findMany({
|
||||
orderBy: { createdAt: "asc" },
|
||||
include: { images: { select: { id: true } } }
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -6,7 +6,8 @@ import Link from "next/link";
|
||||
export default async function CategoriesPage() {
|
||||
const categories = await prisma.category.findMany(
|
||||
{
|
||||
orderBy: { createdAt: "asc" }
|
||||
orderBy: { createdAt: "asc" },
|
||||
include: { images: { select: { id: true } } }
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -9,7 +9,8 @@ export default async function GalleriesEditPage({ params }: { params: { id: stri
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
albums: true
|
||||
albums: { include: { images: true } },
|
||||
coverImage: true
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -4,7 +4,12 @@ import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function GalleriesPage() {
|
||||
const galleries = await prisma.gallery.findMany({ orderBy: { createdAt: "asc" } });
|
||||
const galleries = await prisma.gallery.findMany({
|
||||
orderBy: { createdAt: "asc" },
|
||||
include: {
|
||||
albums: { select: { id: true } }
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -16,27 +16,39 @@ export default async function ImagesEditPage({ params }: { params: { id: string
|
||||
include: {
|
||||
album: true,
|
||||
artist: true,
|
||||
colors: true,
|
||||
extractColors: true,
|
||||
metadata: true,
|
||||
pixels: true,
|
||||
stats: true,
|
||||
theme: true,
|
||||
variants: true,
|
||||
colors: {
|
||||
include: {
|
||||
color: true
|
||||
}
|
||||
},
|
||||
extractColors: {
|
||||
include: {
|
||||
extract: true
|
||||
}
|
||||
},
|
||||
palettes: {
|
||||
include: {
|
||||
palette: {
|
||||
include: {
|
||||
items: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
variants: true,
|
||||
categories: true,
|
||||
tags: true,
|
||||
categories: true
|
||||
}
|
||||
});
|
||||
|
||||
const artists = await prisma.artist.findMany({ orderBy: { createdAt: "asc" } });
|
||||
const albums = await prisma.album.findMany({ orderBy: { createdAt: "asc" }, include: { gallery: true } });
|
||||
const tags = await prisma.tag.findMany({ orderBy: { createdAt: "asc" } });
|
||||
const categories = await prisma.category.findMany({ orderBy: { createdAt: "asc" } });
|
||||
const albums = await prisma.album.findMany({ orderBy: { name: "asc" }, include: { gallery: { select: { name: true } } } });
|
||||
const artists = await prisma.artist.findMany({ orderBy: { displayName: "asc" } });
|
||||
const categories = await prisma.category.findMany({ orderBy: { name: "asc" } });
|
||||
const tags = await prisma.tag.findMany({ orderBy: { name: "asc" } });
|
||||
|
||||
console.log(image)
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
31
src/app/palettes/[id]/page.tsx
Normal file
31
src/app/palettes/[id]/page.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import DisplayPalette from "@/components/palettes/single/DisplayPalette";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function PalettesPage({ params }: { params: { id: string } }) {
|
||||
const { id } = await params;
|
||||
|
||||
const palette = await prisma.colorPalette.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
images: {
|
||||
include: {
|
||||
image: {
|
||||
include: {
|
||||
variants: { where: { type: "thumbnail" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Show palette</h1>
|
||||
{palette ? <DisplayPalette palette={palette} /> : 'Palette not found...'}
|
||||
</div>
|
||||
);
|
||||
}
|
23
src/app/palettes/page.tsx
Normal file
23
src/app/palettes/page.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import ListPalettes from "@/components/palettes/list/ListPalettes";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function PalettesPage() {
|
||||
const palettes = await prisma.colorPalette.findMany(
|
||||
{
|
||||
orderBy: { name: "asc" },
|
||||
include: {
|
||||
items: true,
|
||||
images: { select: { id: true } }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<h1 className="text-2xl font-bold mb-4">Palettes</h1>
|
||||
</div>
|
||||
{palettes.length > 0 ? <ListPalettes palettes={palettes} /> : <p className="text-muted-foreground italic">No palettes found.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -5,15 +5,21 @@ import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Album, Gallery } from "@/generated/prisma";
|
||||
import { Album, Gallery, Image } from "@/generated/prisma";
|
||||
import { albumSchema } from "@/schemas/albums/albumSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import NextImage from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export default function EditAlbumForm({ album, galleries }: { album: Album, galleries: Gallery[] }) {
|
||||
type AlbumWithItems = Album & {
|
||||
images: Image[],
|
||||
coverImage: Image | null
|
||||
}
|
||||
|
||||
export default function EditAlbumForm({ album, galleries }: { album: AlbumWithItems, galleries: Gallery[] }) {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof albumSchema>>({
|
||||
resolver: zodResolver(albumSchema),
|
||||
@ -22,6 +28,7 @@ export default function EditAlbumForm({ album, galleries }: { album: Album, gall
|
||||
slug: album.slug,
|
||||
description: album.description || "",
|
||||
galleryId: album.galleryId || "",
|
||||
coverImageId: album.coverImage?.id || "",
|
||||
},
|
||||
})
|
||||
|
||||
@ -33,6 +40,8 @@ export default function EditAlbumForm({ album, galleries }: { album: Album, gall
|
||||
}
|
||||
}
|
||||
|
||||
const selectedImage = album.images.find(img => img.id === form.watch("coverImageId"));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<Form {...form}>
|
||||
@ -109,6 +118,45 @@ export default function EditAlbumForm({ album, galleries }: { album: Album, gall
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="coverImageId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a cover image" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{album.images.map((image) => (
|
||||
<SelectItem key={image.id} value={image.id}>
|
||||
{image.imageName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Optional cover image shown in album previews.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{selectedImage?.fileKey && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-muted-foreground mb-1">Cover preview:</p>
|
||||
<NextImage
|
||||
src={`/api/image/thumbnails/${selectedImage.fileKey}.webp`}
|
||||
width={128}
|
||||
height={128}
|
||||
alt="Selected cover"
|
||||
className="w-32 h-auto rounded border"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||
|
@ -2,7 +2,11 @@ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/componen
|
||||
import { Artist } from "@/generated/prisma";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ListArtists({ artists }: { artists: Artist[] }) {
|
||||
type ArtistsWithItems = Artist & {
|
||||
images: { id: string }[]
|
||||
}
|
||||
|
||||
export default function ListArtists({ artists }: { artists: ArtistsWithItems[] }) {
|
||||
return (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{artists.map((artist) => (
|
||||
@ -14,6 +18,7 @@ export default function ListArtists({ artists }: { artists: Artist[] }) {
|
||||
<CardContent>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
Connected to {artist.images.length} image{artist.images.length !== 1 ? "s" : ""}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
|
@ -2,7 +2,11 @@ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/componen
|
||||
import { Category } from "@/generated/prisma";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ListCategories({ categories }: { categories: Category[] }) {
|
||||
type CategoriesWithItems = Category & {
|
||||
images: { id: string }[]
|
||||
}
|
||||
|
||||
export default function ListCategories({ categories }: { categories: CategoriesWithItems[] }) {
|
||||
return (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{categories.map((cat) => (
|
||||
@ -15,6 +19,7 @@ export default function ListCategories({ categories }: { categories: Category[]
|
||||
{cat.description && <p className="text-sm text-muted-foreground">{cat.description}</p>}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
Connected to {cat.images.length} image{cat.images.length !== 1 ? "s" : ""}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
|
@ -5,15 +5,26 @@ import { updateGallery } from "@/actions/galleries/updateGallery";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Album, Gallery } from "@/generated/prisma";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Album, Gallery, Image } from "@/generated/prisma";
|
||||
import { gallerySchema } from "@/schemas/galleries/gallerySchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import NextImage from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albums: Album[] } }) {
|
||||
type GalleryWithItems = Gallery & {
|
||||
albums: (Album &
|
||||
{
|
||||
images: Image[]
|
||||
}
|
||||
)[];
|
||||
coverImage: Image | null
|
||||
}
|
||||
|
||||
export default function EditGalleryForm({ gallery }: { gallery: GalleryWithItems }) {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof gallerySchema>>({
|
||||
resolver: zodResolver(gallerySchema),
|
||||
@ -21,6 +32,7 @@ export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albu
|
||||
name: gallery.name,
|
||||
slug: gallery.slug,
|
||||
description: gallery.description || "",
|
||||
coverImageId: gallery.coverImage?.id || "",
|
||||
},
|
||||
})
|
||||
|
||||
@ -32,6 +44,9 @@ export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albu
|
||||
}
|
||||
}
|
||||
|
||||
const allGalleryImages = gallery.albums?.flatMap(a => a.images) || [];
|
||||
const selectedImage = allGalleryImages.find(img => img.id === form.watch("coverImageId"));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<Form {...form}>
|
||||
@ -84,6 +99,44 @@ export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albu
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="coverImageId" // or whatever you store it as
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gallery Cover Image</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select cover image for gallery" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{allGalleryImages.map((img) => (
|
||||
<SelectItem key={img.id} value={img.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate">{img.imageName}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{selectedImage?.fileKey && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-muted-foreground mb-1">Cover preview:</p>
|
||||
<NextImage
|
||||
src={`/api/image/thumbnails/${selectedImage.fileKey}.webp`}
|
||||
width={128}
|
||||
height={128}
|
||||
alt="Selected cover"
|
||||
className="w-32 h-auto rounded border"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||
@ -104,7 +157,7 @@ export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albu
|
||||
</div>
|
||||
<div className="text-sm text-right">
|
||||
{/* Replace this with actual image count later */}
|
||||
<span className="font-mono">Images: 0</span>
|
||||
<span className="font-mono">Images: {album.images.length}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@ -112,7 +165,7 @@ export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albu
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Total images in this gallery: <span className="font-semibold">0</span>
|
||||
Total images in this gallery: <span className="font-semibold">{allGalleryImages.length}</span>
|
||||
</p>
|
||||
<div>
|
||||
{gallery.albums.length === 0 ? (
|
||||
|
@ -1,10 +1,14 @@
|
||||
// "use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Gallery } from "@/generated/prisma";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ListGalleries({ galleries }: { galleries: Gallery[] }) {
|
||||
type GalleriesWithItems = Gallery & {
|
||||
albums: { id: string }[]
|
||||
}
|
||||
|
||||
export default function ListGalleries({ galleries }: { galleries: GalleriesWithItems[] }) {
|
||||
return (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{galleries.map((gallery) => (
|
||||
@ -16,6 +20,9 @@ export default function ListGalleries({ galleries }: { galleries: Gallery[] }) {
|
||||
<CardContent>
|
||||
{gallery.description && <p className="text-sm text-muted-foreground">{gallery.description}</p>}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
Connected to {gallery.albums.length} album{gallery.albums.length !== 1 ? "s" : ""}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
|
@ -37,6 +37,16 @@ export default function TopNav() {
|
||||
<Link href="/tags">Tags</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/palettes">Palettes</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/colors">Colors</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/images">Images</Link>
|
||||
|
39
src/components/images/ImageCard.tsx
Normal file
39
src/components/images/ImageCard.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { Image, ImagePalette, ImageVariant } from "@/generated/prisma";
|
||||
import ImageComponent from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
type ImagePaletteWithItems = {
|
||||
image: ImagePalette & {
|
||||
image: Image & {
|
||||
variants: ImageVariant[];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default function ImageCard({ image }: ImagePaletteWithItems) {
|
||||
const thumbnail = image.image.variants.find((v) => v.type === "thumbnail");
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/images/${image.image.id}`}
|
||||
className="block overflow-hidden rounded-md border shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
{thumbnail?.s3Key ? (
|
||||
<ImageComponent
|
||||
src={`/api/image/${thumbnail.s3Key}`}
|
||||
alt={image.image.altText || image.image.imageName}
|
||||
width={thumbnail.width}
|
||||
height={thumbnail.height}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-48 bg-gray-100 flex items-center justify-center text-muted-foreground text-sm">
|
||||
No Thumbnail
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2 text-sm truncate">{image.image.imageName} ({image.type})</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
@ -9,7 +9,7 @@ import MultipleSelector from "@/components/ui/multiselect";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Album, Artist, Category, ColorPalette, ColorPaletteItem, ExtractColor, Gallery, Image, ImageColor, ImageMetadata, ImageStats, ImageVariant, PixelSummary, Tag, ThemeSeed } from "@/generated/prisma";
|
||||
import { Album, Artist, Category, Color, ColorPalette, ColorPaletteItem, ExtractColor, Image, ImageColor, ImageExtractColor, ImageMetadata, ImagePalette, ImageStats, ImageVariant, Tag } from "@/generated/prisma";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { imageSchema } from "@/schemas/images/imageSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@ -22,33 +22,43 @@ import * as z from "zod/v4";
|
||||
type ImageWithItems = Image & {
|
||||
album: Album | null,
|
||||
artist: Artist | null,
|
||||
colors: ImageColor[],
|
||||
extractColors: ExtractColor[],
|
||||
metadata: ImageMetadata[],
|
||||
pixels: PixelSummary[],
|
||||
stats: ImageStats[],
|
||||
theme: ThemeSeed[],
|
||||
variants: ImageVariant[],
|
||||
tags: Tag[],
|
||||
categories: Category[],
|
||||
metadata: ImageMetadata | null,
|
||||
stats: ImageStats | null,
|
||||
colors: (
|
||||
ImageColor & {
|
||||
color: Color
|
||||
}
|
||||
)[],
|
||||
extractColors: (
|
||||
ImageExtractColor & {
|
||||
extract: ExtractColor
|
||||
}
|
||||
)[],
|
||||
palettes: (
|
||||
ImagePalette & {
|
||||
palette: (
|
||||
ColorPalette & {
|
||||
items: ColorPaletteItem[]
|
||||
}
|
||||
)[]
|
||||
)
|
||||
}
|
||||
)[],
|
||||
variants: ImageVariant[],
|
||||
categories: Category[],
|
||||
tags: Tag[],
|
||||
};
|
||||
|
||||
type AlbumsWithGallery = Album & {
|
||||
gallery: Gallery | null
|
||||
gallery: { name: string } | null
|
||||
}
|
||||
|
||||
export default function EditImageForm({ image, artists, albums, tags, categories }:
|
||||
export default function EditImageForm({ image, albums, artists, categories, tags }:
|
||||
{
|
||||
image: ImageWithItems,
|
||||
artists: Artist[],
|
||||
albums: AlbumsWithGallery[],
|
||||
tags: Tag[],
|
||||
artists: Artist[],
|
||||
categories: Category[]
|
||||
tags: Tag[],
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof imageSchema>>({
|
||||
@ -62,7 +72,7 @@ export default function EditImageForm({ image, artists, albums, tags, categories
|
||||
altText: image.altText || "",
|
||||
description: image.description || "",
|
||||
fileType: image.fileType || "",
|
||||
imageData: image.imageData || "",
|
||||
source: image.source || "",
|
||||
creationMonth: image.creationMonth || undefined,
|
||||
creationYear: image.creationYear || undefined,
|
||||
fileSize: image.fileSize || undefined,
|
||||
@ -192,10 +202,10 @@ export default function EditImageForm({ image, artists, albums, tags, categories
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imageData"
|
||||
name="source"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>imageData</FormLabel>
|
||||
<FormLabel>source</FormLabel>
|
||||
<FormControl><Input {...field} disabled /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
@ -2,11 +2,15 @@
|
||||
|
||||
import { generateExtractColors } from "@/actions/images/generateExtractColors";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExtractColor } from "@/generated/prisma";
|
||||
import { ExtractColor, ImageExtractColor } from "@/generated/prisma";
|
||||
import { useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function ExtractColors({ colors: initialColors, imageId, fileKey }: { colors: ExtractColor[], imageId: string, fileKey: string }) {
|
||||
type ExtractWithJoin = ImageExtractColor & {
|
||||
extract: ExtractColor;
|
||||
};
|
||||
|
||||
export default function ExtractColors({ colors: initialColors, imageId, fileKey }: { colors: ExtractWithJoin[], imageId: string, fileKey: string }) {
|
||||
const [colors, setColors] = useState(initialColors);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
@ -32,8 +36,13 @@ export default function ExtractColors({ colors: initialColors, imageId, fileKey
|
||||
</Button>
|
||||
</div >
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{colors.map((color, index) => (
|
||||
<div key={index} className="w-10 h-10 rounded" style={{ backgroundColor: color.hex }} title={color.hex}></div>
|
||||
{colors.map((item) => (
|
||||
<div
|
||||
key={`${item.imageId}-${item.type}`}
|
||||
className="w-10 h-10 rounded"
|
||||
style={{ backgroundColor: item.extract?.hex ?? "#000000" }}
|
||||
title={`${item.type} – ${item.extract?.hex}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
@ -2,11 +2,15 @@
|
||||
|
||||
import { generateImageColors } from "@/actions/images/generateImageColors";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ImageColor } from "@/generated/prisma";
|
||||
import { Color, ImageColor } from "@/generated/prisma";
|
||||
import { useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function ImageColors({ colors: initialColors, imageId, fileKey }: { colors: ImageColor[], imageId: string, fileKey: string }) {
|
||||
type ColorWithItems = ImageColor & {
|
||||
color: Color
|
||||
};
|
||||
|
||||
export default function ImageColors({ colors: initialColors, imageId, fileKey }: { colors: ColorWithItems[], imageId: string, fileKey: string }) {
|
||||
const [colors, setColors] = useState(initialColors);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
@ -31,15 +35,14 @@ export default function ImageColors({ colors: initialColors, imageId, fileKey }:
|
||||
{isPending ? "Extracting..." : "Generate Palette"}
|
||||
</Button>
|
||||
</div >
|
||||
{/* <h2 className="font-semibold text-lg mb-2">Extracted Colors</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{extractColors.map((color, index) => (
|
||||
<div key={index} className="w-10 h-10 rounded" style={{ backgroundColor: color.hex }} title={color.hex}></div>
|
||||
))}
|
||||
</div> */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{colors.map((color, index) => (
|
||||
<div key={index} className="w-10 h-10 rounded" style={{ backgroundColor: color.hex ?? "#000000" }} title={`Tone ${color.type} - ${color.hex}`}></div>
|
||||
{colors.map((item) => (
|
||||
<div
|
||||
key={`${item.imageId}-${item.type}`}
|
||||
className="w-10 h-10 rounded"
|
||||
style={{ backgroundColor: item.color?.hex ?? "#000000" }}
|
||||
title={`${item.type} – ${item.color?.hex}`}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
@ -2,12 +2,14 @@
|
||||
|
||||
import { generatePaletteAction } from "@/actions/images/generatePalette";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ColorPalette, ColorPaletteItem } from "@/generated/prisma";
|
||||
import { ColorPalette, ColorPaletteItem, ImagePalette } from "@/generated/prisma";
|
||||
import { useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type PaletteWithItems = ColorPalette & {
|
||||
items: ColorPaletteItem[];
|
||||
type PaletteWithItems = ImagePalette & {
|
||||
palette: ColorPalette & {
|
||||
items: ColorPaletteItem[]
|
||||
};
|
||||
};
|
||||
|
||||
export default function ImagePalettes({ palettes: initialPalettes, imageId, fileKey }: { palettes: PaletteWithItems[], imageId: string, fileKey: string }) {
|
||||
@ -37,12 +39,12 @@ export default function ImagePalettes({ palettes: initialPalettes, imageId, file
|
||||
</div >
|
||||
|
||||
<div className="space-y-4">
|
||||
{palettes.map((palette) => (
|
||||
palette.type != 'error' ?
|
||||
<div key={palette.id}>
|
||||
<div className="text-sm font-medium mb-1">{palette.type}</div>
|
||||
{palettes.map((imagePalette) => (
|
||||
imagePalette.type != 'error' ?
|
||||
<div key={imagePalette.id}>
|
||||
<div className="text-sm font-medium mb-1">{imagePalette.type}</div>
|
||||
<div className="flex gap-1">
|
||||
{palette.items
|
||||
{imagePalette.palette.items
|
||||
.filter((item) => item.tone !== null && item.hex !== null)
|
||||
.sort((a, b) => (a.tone ?? 0) - (b.tone ?? 0))
|
||||
.map((item) => (
|
||||
|
39
src/components/palettes/list/ListPalettes.tsx
Normal file
39
src/components/palettes/list/ListPalettes.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ColorPalette, ColorPaletteItem } from "@/generated/prisma";
|
||||
import Link from "next/link";
|
||||
|
||||
type PalettesWithItems = ColorPalette & {
|
||||
items: ColorPaletteItem[],
|
||||
images: { id: string }[]
|
||||
}
|
||||
|
||||
export default function ListPalettes({ palettes }: { palettes: PalettesWithItems[] }) {
|
||||
return (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{palettes.map((palette) => (
|
||||
<Link href={`/palettes/${palette.id}`} key={palette.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{palette.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{palette.items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="w-10 h-10 rounded-full border"
|
||||
style={{ backgroundColor: item.hex ?? "#ccc" }}
|
||||
title={item.hex ?? "n/a"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
Used by {palette.images.length} image{palette.images.length !== 1 ? "s" : ""}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
45
src/components/palettes/single/DisplayPalette.tsx
Normal file
45
src/components/palettes/single/DisplayPalette.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
|
||||
import ImageCard from "@/components/images/ImageCard";
|
||||
import { ColorPalette, ColorPaletteItem, Image, ImagePalette, ImageVariant } from "@/generated/prisma";
|
||||
|
||||
type PaletteWithItems = ColorPalette & {
|
||||
items: ColorPaletteItem[],
|
||||
images: (ImagePalette & {
|
||||
image: Image & {
|
||||
variants: ImageVariant[]
|
||||
}
|
||||
})[]
|
||||
}
|
||||
|
||||
export default function DisplayPalette({ palette }: { palette: PaletteWithItems }) {
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{palette.name}</h1>
|
||||
{/* <p className="text-muted-foreground">Type: {palette.type}</p> */}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{palette.items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="w-10 h-10 rounded-full border"
|
||||
style={{ backgroundColor: item.hex ?? "#ccc" }}
|
||||
title={item.hex ?? "n/a"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Used by {palette.images.length} image(s)</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-2">
|
||||
{palette.images.map((image) => (
|
||||
<ImageCard key={image.id} image={image} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -5,5 +5,6 @@ export const albumSchema = z.object({
|
||||
slug: z.string().min(3, "Slug is required. Min 3 characters.").regex(/^[a-z]+$/, "Only lowercase letters are allowed (no numbers, spaces, or uppercase)"),
|
||||
galleryId: z.string().min(1, "Please select a gallery"),
|
||||
description: z.string().optional(),
|
||||
coverImageId: z.string().optional(),
|
||||
})
|
||||
|
||||
|
@ -4,4 +4,5 @@ export const gallerySchema = z.object({
|
||||
name: z.string().min(3, "Name is required. Min 3 characters."),
|
||||
slug: z.string().min(3, "Slug is required. Min 3 characters.").regex(/^[a-z]+$/, "Only lowercase letters are allowed (no numbers, spaces, or uppercase)"),
|
||||
description: z.string().optional(),
|
||||
coverImageId: z.string().optional(),
|
||||
})
|
@ -18,15 +18,15 @@ export const imageSchema = z.object({
|
||||
altText: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
fileType: z.string().optional(),
|
||||
imageData: z.string().optional(),
|
||||
source: z.string().optional(),
|
||||
|
||||
creationMonth: z.number().min(1).max(12).optional(),
|
||||
creationYear: z.number().min(1900).max(2100).optional(),
|
||||
fileSize: z.number().optional(),
|
||||
creationDate: z.date().optional(),
|
||||
|
||||
artistId: z.string().optional(),
|
||||
albumId: z.string().optional(),
|
||||
tagIds: z.array(z.string()).optional(),
|
||||
artistId: z.string().optional(),
|
||||
categoryIds: z.array(z.string()).optional(),
|
||||
tagIds: z.array(z.string()).optional(),
|
||||
})
|
@ -44,42 +44,56 @@ export async function upsertPalettes(tones: Tone[], imageId: string, type: strin
|
||||
|
||||
const existingPalette = await prisma.colorPalette.findFirst({
|
||||
where: { name: paletteName },
|
||||
include: { images: { select: { id: true } } },
|
||||
});
|
||||
|
||||
if (existingPalette) {
|
||||
const alreadyLinked = existingPalette.images.some(img => img.id === imageId);
|
||||
|
||||
if (!alreadyLinked) {
|
||||
await prisma.colorPalette.update({
|
||||
where: { id: existingPalette.id },
|
||||
data: {
|
||||
images: {
|
||||
connect: { id: imageId },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return existingPalette;
|
||||
}
|
||||
|
||||
const newPalette = await prisma.colorPalette.create({
|
||||
data: {
|
||||
name: paletteName,
|
||||
type: type,
|
||||
items: {
|
||||
create: tones.map(t => ({
|
||||
tone: t.tone,
|
||||
hex: t.hex,
|
||||
})),
|
||||
},
|
||||
images: {
|
||||
connect: { id: imageId },
|
||||
},
|
||||
},
|
||||
include: { items: true },
|
||||
});
|
||||
|
||||
return newPalette;
|
||||
//
|
||||
const palette = existingPalette ?? await prisma.colorPalette.create({
|
||||
data: {
|
||||
name: paletteName,
|
||||
items: {
|
||||
create: tones.map(tone => ({
|
||||
tone: tone.tone,
|
||||
hex: tone.hex,
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.imagePalette.upsert({
|
||||
where: {
|
||||
imageId_type: {
|
||||
imageId,
|
||||
type,
|
||||
}
|
||||
},
|
||||
update: {
|
||||
paletteId: palette.id
|
||||
},
|
||||
create: {
|
||||
imageId,
|
||||
paletteId: palette.id,
|
||||
type,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// const newPalette = await prisma.colorPalette.create({
|
||||
// data: {
|
||||
// name: paletteName,
|
||||
// type: type,
|
||||
// items: {
|
||||
// create: tones.map(t => ({
|
||||
// tone: t.tone,
|
||||
// hex: t.hex,
|
||||
// })),
|
||||
// },
|
||||
// images: {
|
||||
// connect: { id: imageId },
|
||||
// },
|
||||
// },
|
||||
// include: { items: true },
|
||||
// });
|
||||
|
||||
return palette;
|
||||
}
|
Reference in New Issue
Block a user