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?
|
description String?
|
||||||
|
|
||||||
// coverImageId String?
|
coverImageId String?
|
||||||
// coverImage Image? @relation("GalleryCoverImage", fields: [coverImageId], references: [id])
|
coverImage Image? @relation("GalleryCoverImage", fields: [coverImageId], references: [id])
|
||||||
|
|
||||||
albums Album[]
|
albums Album[]
|
||||||
}
|
}
|
||||||
@ -35,17 +35,19 @@ model Album {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
name String
|
|
||||||
slug String
|
slug String
|
||||||
|
name String
|
||||||
|
|
||||||
description String?
|
description String?
|
||||||
|
|
||||||
// coverImageId String?
|
coverImageId String?
|
||||||
galleryId 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])
|
gallery Gallery? @relation(fields: [galleryId], references: [id])
|
||||||
|
|
||||||
images Image[]
|
images Image[]
|
||||||
|
|
||||||
|
@@unique([galleryId, slug])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Artist {
|
model Artist {
|
||||||
@ -77,6 +79,30 @@ model Social {
|
|||||||
artist Artist? @relation(fields: [artistId], references: [id])
|
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 {
|
model Image {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@ -91,6 +117,7 @@ model Image {
|
|||||||
description String?
|
description String?
|
||||||
fileType String?
|
fileType String?
|
||||||
imageData String?
|
imageData String?
|
||||||
|
source String?
|
||||||
creationMonth Int?
|
creationMonth Int?
|
||||||
creationYear Int?
|
creationYear Int?
|
||||||
fileSize Int?
|
fileSize Int?
|
||||||
@ -100,22 +127,22 @@ model Image {
|
|||||||
artistId String?
|
artistId String?
|
||||||
album Album? @relation(fields: [albumId], references: [id])
|
album Album? @relation(fields: [albumId], references: [id])
|
||||||
artist Artist? @relation(fields: [artistId], references: [id])
|
artist Artist? @relation(fields: [artistId], references: [id])
|
||||||
// sourceId String?
|
|
||||||
// source Source? @relation(fields: [sourceId], references: [id])
|
|
||||||
|
|
||||||
metadata ImageMetadata[]
|
metadata ImageMetadata?
|
||||||
pixels PixelSummary[]
|
stats ImageStats?
|
||||||
stats ImageStats[]
|
|
||||||
theme ThemeSeed[]
|
|
||||||
variants ImageVariant[]
|
|
||||||
|
|
||||||
// albumCover Album[] @relation("AlbumCoverImage")
|
colors ImageColor[]
|
||||||
// galleryCover Gallery[] @relation("GalleryCoverImage")
|
extractColors ImageExtractColor[]
|
||||||
categories Category[] @relation("ImageCategories")
|
palettes ImagePalette[]
|
||||||
colors ImageColor[] @relation("ImageToImageColor")
|
variants ImageVariant[]
|
||||||
extractColors ExtractColor[] @relation("ImageToExtractColor")
|
// pixels PixelSummary[]
|
||||||
palettes ColorPalette[] @relation("ImagePalettes")
|
// theme ThemeSeed[]
|
||||||
tags Tag[] @relation("ImageTags")
|
albumCover Album[] @relation("AlbumCoverImage")
|
||||||
|
galleryCover Gallery[] @relation("GalleryCoverImage")
|
||||||
|
categories Category[] @relation("ImageCategories")
|
||||||
|
// colors ImageColor[] @relation("ImageToImageColor")
|
||||||
|
tags Tag[] @relation("ImageTags")
|
||||||
|
// palettes ColorPalette[] @relation("ImagePalettes")
|
||||||
}
|
}
|
||||||
|
|
||||||
model ImageMetadata {
|
model ImageMetadata {
|
||||||
@ -123,7 +150,7 @@ model ImageMetadata {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
imageId String
|
imageId String @unique
|
||||||
depth String
|
depth String
|
||||||
format String
|
format String
|
||||||
space String
|
space String
|
||||||
@ -148,7 +175,7 @@ model ImageStats {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
imageId String
|
imageId String @unique
|
||||||
entropy Float
|
entropy Float
|
||||||
sharpness Float
|
sharpness Float
|
||||||
dominantB Int
|
dominantB Int
|
||||||
@ -184,10 +211,10 @@ model ColorPalette {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
name String
|
name String
|
||||||
type String
|
|
||||||
|
|
||||||
items ColorPaletteItem[]
|
items ColorPaletteItem[]
|
||||||
images Image[] @relation("ImagePalettes")
|
images ImagePalette[]
|
||||||
|
// images Image[] @relation("ImagePalettes")
|
||||||
}
|
}
|
||||||
|
|
||||||
model ColorPaletteItem {
|
model ColorPaletteItem {
|
||||||
@ -217,10 +244,11 @@ model ExtractColor {
|
|||||||
hue Float?
|
hue Float?
|
||||||
saturation Float?
|
saturation Float?
|
||||||
|
|
||||||
images Image[] @relation("ImageToExtractColor")
|
// images Image[] @relation("ImageToExtractColor")
|
||||||
|
images ImageExtractColor[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model ImageColor {
|
model Color {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@ -233,53 +261,74 @@ model ImageColor {
|
|||||||
green Int?
|
green Int?
|
||||||
red 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
|
||||||
|
paletteId String
|
||||||
|
type String
|
||||||
|
|
||||||
|
image Image @relation(fields: [imageId], references: [id])
|
||||||
|
palette ColorPalette @relation(fields: [paletteId], references: [id])
|
||||||
|
|
||||||
|
@@unique([imageId, type])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ImageExtractColor {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
imageId String
|
||||||
|
extractId String
|
||||||
|
type String
|
||||||
|
|
||||||
|
image Image @relation(fields: [imageId], references: [id])
|
||||||
|
extract ExtractColor @relation(fields: [extractId], references: [id])
|
||||||
|
|
||||||
|
@@unique([imageId, type])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ImageColor {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
imageId String
|
imageId String
|
||||||
seedHex String
|
colorId String
|
||||||
|
type String
|
||||||
|
|
||||||
image Image @relation(fields: [imageId], references: [id])
|
image Image @relation(fields: [imageId], references: [id])
|
||||||
}
|
color Color @relation(fields: [colorId], references: [id])
|
||||||
|
|
||||||
model PixelSummary {
|
@@unique([imageId, type])
|
||||||
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 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")
|
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,8 @@ export async function updateAlbum(
|
|||||||
name: values.name,
|
name: values.name,
|
||||||
slug: values.slug,
|
slug: values.slug,
|
||||||
description: values.description,
|
description: values.description,
|
||||||
galleryId: values.galleryId
|
galleryId: values.galleryId,
|
||||||
|
coverImageId: values.coverImageId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -16,6 +16,7 @@ export async function updateGallery(
|
|||||||
name: values.name,
|
name: values.name,
|
||||||
slug: values.slug,
|
slug: values.slug,
|
||||||
description: values.description,
|
description: values.description,
|
||||||
|
coverImageId: values.coverImageId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -16,15 +16,16 @@ export async function generateExtractColors(imageId: string, fileKey: string) {
|
|||||||
metadata: true
|
metadata: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const buffer = await getImageBufferFromS3(fileKey);
|
|
||||||
if(!image) throw new Error("Image not found");
|
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 imageDataUrl = `data:${image.fileType};base64,${buffer.toString("base64")}`;
|
||||||
|
|
||||||
const pixels = await new Promise<NdArray<Uint8Array>>((resolve, reject) => {
|
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);
|
if (err) reject(err);
|
||||||
else resolve(pixels);
|
else resolve(result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -34,36 +35,46 @@ export async function generateExtractColors(imageId: string, fileKey: string) {
|
|||||||
height: pixels.shape[1]
|
height: pixels.shape[1]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let typeIndex = 0;
|
||||||
|
|
||||||
for (const c of extracted) {
|
for (const c of extracted) {
|
||||||
const name = generateExtractColorName(c.hex, c.hue, c.saturation, c.area);
|
const name = generateExtractColorName(c.hex, c.hue, c.saturation, c.area);
|
||||||
|
|
||||||
await prisma.image.update({
|
const extract = await prisma.extractColor.upsert({
|
||||||
where: { id: imageId },
|
where: { name },
|
||||||
data: {
|
create: {
|
||||||
extractColors: {
|
name,
|
||||||
connectOrCreate: {
|
hex: c.hex,
|
||||||
where: { name },
|
red: c.red,
|
||||||
create: {
|
green: c.green,
|
||||||
name,
|
blue: c.blue,
|
||||||
hex: c.hex,
|
hue: c.hue,
|
||||||
red: c.red,
|
saturation: c.saturation,
|
||||||
green: c.green,
|
area: c.area,
|
||||||
blue: c.blue,
|
},
|
||||||
hue: c.hue,
|
update: {},
|
||||||
saturation: c.saturation,
|
});
|
||||||
area: c.area,
|
|
||||||
},
|
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({
|
return await prisma.imageExtractColor.findMany({
|
||||||
where: {
|
where: { imageId },
|
||||||
images: {
|
include: { extract: true },
|
||||||
some: { id: imageId },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -10,45 +10,57 @@ export async function generateImageColors(imageId: string, fileKey: string) {
|
|||||||
const buffer = await getImageBufferFromS3(fileKey);
|
const buffer = await getImageBufferFromS3(fileKey);
|
||||||
const palette = await Vibrant.from(buffer).getPalette();
|
const palette = await Vibrant.from(buffer).getPalette();
|
||||||
|
|
||||||
const vibrantHexes = Object.fromEntries(
|
const vibrantHexes = Object.entries(palette).map(([key, swatch]) => {
|
||||||
Object.entries(palette).map(([key, swatch]) => {
|
const castSwatch = swatch as VibrantSwatch | null;
|
||||||
const castSwatch = swatch as VibrantSwatch | null;
|
const rgb = castSwatch?._rgb;
|
||||||
const rgb = castSwatch?._rgb;
|
const hex = castSwatch?.hex || (rgb ? rgbToHex(rgb) : undefined);
|
||||||
const hex = castSwatch?.hex || (rgb ? rgbToHex(rgb) : undefined);
|
return { type: key, hex };
|
||||||
return [key, hex];
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [type, hex] of Object.entries(vibrantHexes)) {
|
for (const { type, hex } of vibrantHexes) {
|
||||||
if (!hex) continue;
|
if (!hex) continue;
|
||||||
|
|
||||||
const [r, g, b] = hex.match(/\w\w/g)!.map((h) => parseInt(h, 16));
|
const [r, g, b] = hex.match(/\w\w/g)!.map((h) => parseInt(h, 16));
|
||||||
const name = generateColorName(hex);
|
const name = generateColorName(hex);
|
||||||
|
|
||||||
await prisma.image.update({
|
const color = await prisma.color.upsert({
|
||||||
where: { id: imageId },
|
where: { name },
|
||||||
data: {
|
create: {
|
||||||
colors: {
|
name,
|
||||||
connectOrCreate: {
|
type,
|
||||||
where: { name: name },
|
hex,
|
||||||
create: {
|
red: r,
|
||||||
name: name,
|
green: g,
|
||||||
type: type,
|
blue: b,
|
||||||
hex: hex,
|
},
|
||||||
red: r,
|
update: {
|
||||||
green: g,
|
hex,
|
||||||
blue: b,
|
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({
|
return await prisma.imageColor.findMany({
|
||||||
where: {
|
where: { imageId },
|
||||||
images: {
|
include: { color: true },
|
||||||
some: { id: imageId },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -44,15 +44,16 @@ export async function generatePaletteAction(imageId: string, fileKey: string) {
|
|||||||
await upsertPalettes(neutralVariantTones, imageId, "neutralVariant");
|
await upsertPalettes(neutralVariantTones, imageId, "neutralVariant");
|
||||||
await upsertPalettes(errorTones, imageId, "error");
|
await upsertPalettes(errorTones, imageId, "error");
|
||||||
|
|
||||||
await prisma.themeSeed.create({
|
return await prisma.imagePalette.findMany({
|
||||||
data: {
|
where: {
|
||||||
seedHex,
|
imageId: imageId
|
||||||
imageId,
|
|
||||||
},
|
},
|
||||||
});
|
include: {
|
||||||
|
palette: {
|
||||||
return await prisma.colorPalette.findMany({
|
include: {
|
||||||
where: { images: { some: { id: imageId } } },
|
items: true
|
||||||
include: { items: true },
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -7,6 +7,10 @@ export default async function AlbumsEditPage({ params }: { params: { id: string
|
|||||||
const album = await prisma.album.findUnique({
|
const album = await prisma.album.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
coverImage: true,
|
||||||
|
images: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ export default async function AlbumsPage() {
|
|||||||
const albums = await prisma.album.findMany(
|
const albums = await prisma.album.findMany(
|
||||||
{
|
{
|
||||||
include: { gallery: true, images: { select: { id: true } } },
|
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";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function ArtistsPage() {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -6,7 +6,8 @@ import Link from "next/link";
|
|||||||
export default async function CategoriesPage() {
|
export default async function CategoriesPage() {
|
||||||
const categories = await prisma.category.findMany(
|
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,
|
id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
albums: true
|
albums: { include: { images: true } },
|
||||||
|
coverImage: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,7 +4,12 @@ import { PlusCircleIcon } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function GalleriesPage() {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -16,27 +16,39 @@ export default async function ImagesEditPage({ params }: { params: { id: string
|
|||||||
include: {
|
include: {
|
||||||
album: true,
|
album: true,
|
||||||
artist: true,
|
artist: true,
|
||||||
colors: true,
|
|
||||||
extractColors: true,
|
|
||||||
metadata: true,
|
metadata: true,
|
||||||
pixels: true,
|
|
||||||
stats: true,
|
stats: true,
|
||||||
theme: true,
|
colors: {
|
||||||
variants: true,
|
|
||||||
palettes: {
|
|
||||||
include: {
|
include: {
|
||||||
items: true
|
color: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
extractColors: {
|
||||||
|
include: {
|
||||||
|
extract: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
palettes: {
|
||||||
|
include: {
|
||||||
|
palette: {
|
||||||
|
include: {
|
||||||
|
items: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variants: true,
|
||||||
|
categories: true,
|
||||||
tags: true,
|
tags: true,
|
||||||
categories: true
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const artists = await prisma.artist.findMany({ orderBy: { createdAt: "asc" } });
|
const albums = await prisma.album.findMany({ orderBy: { name: "asc" }, include: { gallery: { select: { name: true } } } });
|
||||||
const albums = await prisma.album.findMany({ orderBy: { createdAt: "asc" }, include: { gallery: true } });
|
const artists = await prisma.artist.findMany({ orderBy: { displayName: "asc" } });
|
||||||
const tags = await prisma.tag.findMany({ orderBy: { createdAt: "asc" } });
|
const categories = await prisma.category.findMany({ orderBy: { name: "asc" } });
|
||||||
const categories = await prisma.category.findMany({ orderBy: { createdAt: "asc" } });
|
const tags = await prisma.tag.findMany({ orderBy: { name: "asc" } });
|
||||||
|
|
||||||
|
console.log(image)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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 { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
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 { albumSchema } from "@/schemas/albums/albumSchema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import NextImage from "next/image";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import * as z from "zod/v4";
|
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 router = useRouter();
|
||||||
const form = useForm<z.infer<typeof albumSchema>>({
|
const form = useForm<z.infer<typeof albumSchema>>({
|
||||||
resolver: zodResolver(albumSchema),
|
resolver: zodResolver(albumSchema),
|
||||||
@ -22,6 +28,7 @@ export default function EditAlbumForm({ album, galleries }: { album: Album, gall
|
|||||||
slug: album.slug,
|
slug: album.slug,
|
||||||
description: album.description || "",
|
description: album.description || "",
|
||||||
galleryId: album.galleryId || "",
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@ -109,6 +118,45 @@ export default function EditAlbumForm({ album, galleries }: { album: Album, gall
|
|||||||
</FormItem>
|
</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">
|
<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>
|
||||||
|
@ -2,7 +2,11 @@ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/componen
|
|||||||
import { Artist } from "@/generated/prisma";
|
import { Artist } from "@/generated/prisma";
|
||||||
import Link from "next/link";
|
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 (
|
return (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||||
{artists.map((artist) => (
|
{artists.map((artist) => (
|
||||||
@ -14,6 +18,7 @@ export default function ListArtists({ artists }: { artists: Artist[] }) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
|
Connected to {artist.images.length} image{artist.images.length !== 1 ? "s" : ""}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -2,7 +2,11 @@ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/componen
|
|||||||
import { Category } from "@/generated/prisma";
|
import { Category } from "@/generated/prisma";
|
||||||
import Link from "next/link";
|
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 (
|
return (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||||
{categories.map((cat) => (
|
{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>}
|
{cat.description && <p className="text-sm text-muted-foreground">{cat.description}</p>}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
|
Connected to {cat.images.length} image{cat.images.length !== 1 ? "s" : ""}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -5,15 +5,26 @@ import { updateGallery } from "@/actions/galleries/updateGallery";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
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 { gallerySchema } from "@/schemas/galleries/gallerySchema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import NextImage from "next/image";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import * as z from "zod/v4";
|
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 router = useRouter();
|
||||||
const form = useForm<z.infer<typeof gallerySchema>>({
|
const form = useForm<z.infer<typeof gallerySchema>>({
|
||||||
resolver: zodResolver(gallerySchema),
|
resolver: zodResolver(gallerySchema),
|
||||||
@ -21,6 +32,7 @@ export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albu
|
|||||||
name: gallery.name,
|
name: gallery.name,
|
||||||
slug: gallery.slug,
|
slug: gallery.slug,
|
||||||
description: gallery.description || "",
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@ -84,6 +99,44 @@ export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albu
|
|||||||
</FormItem>
|
</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">
|
<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>
|
||||||
@ -104,7 +157,7 @@ export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albu
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-right">
|
<div className="text-sm text-right">
|
||||||
{/* Replace this with actual image count later */}
|
{/* Replace this with actual image count later */}
|
||||||
<span className="font-mono">Images: 0</span>
|
<span className="font-mono">Images: {album.images.length}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@ -112,7 +165,7 @@ export default function EditGalleryForm({ gallery }: { gallery: Gallery & { albu
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<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>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
{gallery.albums.length === 0 ? (
|
{gallery.albums.length === 0 ? (
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
// "use client"
|
// "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 { Gallery } from "@/generated/prisma";
|
||||||
import Link from "next/link";
|
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 (
|
return (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||||
{galleries.map((gallery) => (
|
{galleries.map((gallery) => (
|
||||||
@ -16,6 +20,9 @@ export default function ListGalleries({ galleries }: { galleries: Gallery[] }) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
{gallery.description && <p className="text-sm text-muted-foreground">{gallery.description}</p>}
|
{gallery.description && <p className="text-sm text-muted-foreground">{gallery.description}</p>}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
Connected to {gallery.albums.length} album{gallery.albums.length !== 1 ? "s" : ""}
|
||||||
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
@ -37,6 +37,16 @@ export default function TopNav() {
|
|||||||
<Link href="/tags">Tags</Link>
|
<Link href="/tags">Tags</Link>
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
</NavigationMenuItem>
|
</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>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||||
<Link href="/images">Images</Link>
|
<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 { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
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 { cn } from "@/lib/utils";
|
||||||
import { imageSchema } from "@/schemas/images/imageSchema";
|
import { imageSchema } from "@/schemas/images/imageSchema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@ -22,33 +22,43 @@ import * as z from "zod/v4";
|
|||||||
type ImageWithItems = Image & {
|
type ImageWithItems = Image & {
|
||||||
album: Album | null,
|
album: Album | null,
|
||||||
artist: Artist | null,
|
artist: Artist | null,
|
||||||
colors: ImageColor[],
|
metadata: ImageMetadata | null,
|
||||||
extractColors: ExtractColor[],
|
stats: ImageStats | null,
|
||||||
metadata: ImageMetadata[],
|
colors: (
|
||||||
pixels: PixelSummary[],
|
ImageColor & {
|
||||||
stats: ImageStats[],
|
color: Color
|
||||||
theme: ThemeSeed[],
|
|
||||||
variants: ImageVariant[],
|
|
||||||
tags: Tag[],
|
|
||||||
categories: Category[],
|
|
||||||
palettes: (
|
|
||||||
ColorPalette & {
|
|
||||||
items: ColorPaletteItem[]
|
|
||||||
}
|
}
|
||||||
)[]
|
)[],
|
||||||
|
extractColors: (
|
||||||
|
ImageExtractColor & {
|
||||||
|
extract: ExtractColor
|
||||||
|
}
|
||||||
|
)[],
|
||||||
|
palettes: (
|
||||||
|
ImagePalette & {
|
||||||
|
palette: (
|
||||||
|
ColorPalette & {
|
||||||
|
items: ColorPaletteItem[]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)[],
|
||||||
|
variants: ImageVariant[],
|
||||||
|
categories: Category[],
|
||||||
|
tags: Tag[],
|
||||||
};
|
};
|
||||||
|
|
||||||
type AlbumsWithGallery = Album & {
|
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,
|
image: ImageWithItems,
|
||||||
artists: Artist[],
|
|
||||||
albums: AlbumsWithGallery[],
|
albums: AlbumsWithGallery[],
|
||||||
tags: Tag[],
|
artists: Artist[],
|
||||||
categories: Category[]
|
categories: Category[]
|
||||||
|
tags: Tag[],
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<z.infer<typeof imageSchema>>({
|
const form = useForm<z.infer<typeof imageSchema>>({
|
||||||
@ -62,7 +72,7 @@ export default function EditImageForm({ image, artists, albums, tags, categories
|
|||||||
altText: image.altText || "",
|
altText: image.altText || "",
|
||||||
description: image.description || "",
|
description: image.description || "",
|
||||||
fileType: image.fileType || "",
|
fileType: image.fileType || "",
|
||||||
imageData: image.imageData || "",
|
source: image.source || "",
|
||||||
creationMonth: image.creationMonth || undefined,
|
creationMonth: image.creationMonth || undefined,
|
||||||
creationYear: image.creationYear || undefined,
|
creationYear: image.creationYear || undefined,
|
||||||
fileSize: image.fileSize || undefined,
|
fileSize: image.fileSize || undefined,
|
||||||
@ -192,10 +202,10 @@ export default function EditImageForm({ image, artists, albums, tags, categories
|
|||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="imageData"
|
name="source"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>imageData</FormLabel>
|
<FormLabel>source</FormLabel>
|
||||||
<FormControl><Input {...field} disabled /></FormControl>
|
<FormControl><Input {...field} disabled /></FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
import { generateExtractColors } from "@/actions/images/generateExtractColors";
|
import { generateExtractColors } from "@/actions/images/generateExtractColors";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ExtractColor } from "@/generated/prisma";
|
import { ExtractColor, ImageExtractColor } from "@/generated/prisma";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { toast } from "sonner";
|
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 [colors, setColors] = useState(initialColors);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
@ -32,8 +36,13 @@ export default function ExtractColors({ colors: initialColors, imageId, fileKey
|
|||||||
</Button>
|
</Button>
|
||||||
</div >
|
</div >
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{colors.map((color, index) => (
|
{colors.map((item) => (
|
||||||
<div key={index} className="w-10 h-10 rounded" style={{ backgroundColor: color.hex }} title={color.hex}></div>
|
<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>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
import { generateImageColors } from "@/actions/images/generateImageColors";
|
import { generateImageColors } from "@/actions/images/generateImageColors";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ImageColor } from "@/generated/prisma";
|
import { Color, ImageColor } from "@/generated/prisma";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { toast } from "sonner";
|
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 [colors, setColors] = useState(initialColors);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
@ -31,15 +35,14 @@ export default function ImageColors({ colors: initialColors, imageId, fileKey }:
|
|||||||
{isPending ? "Extracting..." : "Generate Palette"}
|
{isPending ? "Extracting..." : "Generate Palette"}
|
||||||
</Button>
|
</Button>
|
||||||
</div >
|
</div >
|
||||||
{/* <h2 className="font-semibold text-lg mb-2">Extracted Colors</h2>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{extractColors.map((color, index) => (
|
{colors.map((item) => (
|
||||||
<div key={index} className="w-10 h-10 rounded" style={{ backgroundColor: color.hex }} title={color.hex}></div>
|
<div
|
||||||
))}
|
key={`${item.imageId}-${item.type}`}
|
||||||
</div> */}
|
className="w-10 h-10 rounded"
|
||||||
<div className="flex flex-wrap gap-2">
|
style={{ backgroundColor: item.color?.hex ?? "#000000" }}
|
||||||
{colors.map((color, index) => (
|
title={`${item.type} – ${item.color?.hex}`}
|
||||||
<div key={index} className="w-10 h-10 rounded" style={{ backgroundColor: color.hex ?? "#000000" }} title={`Tone ${color.type} - ${color.hex}`}></div>
|
></div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
import { generatePaletteAction } from "@/actions/images/generatePalette";
|
import { generatePaletteAction } from "@/actions/images/generatePalette";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { useState, useTransition } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
type PaletteWithItems = ColorPalette & {
|
type PaletteWithItems = ImagePalette & {
|
||||||
items: ColorPaletteItem[];
|
palette: ColorPalette & {
|
||||||
|
items: ColorPaletteItem[]
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ImagePalettes({ palettes: initialPalettes, imageId, fileKey }: { palettes: PaletteWithItems[], imageId: string, fileKey: string }) {
|
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 >
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{palettes.map((palette) => (
|
{palettes.map((imagePalette) => (
|
||||||
palette.type != 'error' ?
|
imagePalette.type != 'error' ?
|
||||||
<div key={palette.id}>
|
<div key={imagePalette.id}>
|
||||||
<div className="text-sm font-medium mb-1">{palette.type}</div>
|
<div className="text-sm font-medium mb-1">{imagePalette.type}</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{palette.items
|
{imagePalette.palette.items
|
||||||
.filter((item) => item.tone !== null && item.hex !== null)
|
.filter((item) => item.tone !== null && item.hex !== null)
|
||||||
.sort((a, b) => (a.tone ?? 0) - (b.tone ?? 0))
|
.sort((a, b) => (a.tone ?? 0) - (b.tone ?? 0))
|
||||||
.map((item) => (
|
.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)"),
|
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"),
|
galleryId: z.string().min(1, "Please select a gallery"),
|
||||||
description: z.string().optional(),
|
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."),
|
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)"),
|
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(),
|
description: z.string().optional(),
|
||||||
|
coverImageId: z.string().optional(),
|
||||||
})
|
})
|
@ -18,15 +18,15 @@ export const imageSchema = z.object({
|
|||||||
altText: z.string().optional(),
|
altText: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
fileType: z.string().optional(),
|
fileType: z.string().optional(),
|
||||||
imageData: z.string().optional(),
|
source: z.string().optional(),
|
||||||
|
|
||||||
creationMonth: z.number().min(1).max(12).optional(),
|
creationMonth: z.number().min(1).max(12).optional(),
|
||||||
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(),
|
||||||
|
|
||||||
artistId: z.string().optional(),
|
|
||||||
albumId: z.string().optional(),
|
albumId: z.string().optional(),
|
||||||
tagIds: z.array(z.string()).optional(),
|
artistId: z.string().optional(),
|
||||||
categoryIds: z.array(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({
|
const existingPalette = await prisma.colorPalette.findFirst({
|
||||||
where: { name: paletteName },
|
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 },
|
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