Add image upload function
This commit is contained in:
@ -4,4 +4,15 @@ const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '100mb',
|
||||
},
|
||||
},
|
||||
images: {
|
||||
contentDispositionType: 'inline',
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig;
|
||||
|
3000
package-lock.json
generated
3000
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -3,13 +3,16 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "PORT=3001 next dev --turbopack",
|
||||
"dev": "PORT=3001 NODE_OPTIONS='--max-old-space-size=4096' next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.837.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.837.0",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@prisma/client": "^6.10.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
@ -18,20 +21,28 @@
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"extract-colors": "^4.2.0",
|
||||
"get-pixels": "^3.3.3",
|
||||
"lucide-react": "^0.523.0",
|
||||
"ndarray": "^1.0.19",
|
||||
"next": "15.3.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-vibrant": "^4.0.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"sharp": "^0.34.2",
|
||||
"sonner": "^2.0.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/get-pixels": "^3.3.4",
|
||||
"@types/ndarray": "^1.0.14",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
26
prisma/migrations/20250625150825_image/migration.sql
Normal file
26
prisma/migrations/20250625150825_image/migration.sql
Normal file
@ -0,0 +1,26 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Image" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"fileKey" TEXT NOT NULL,
|
||||
"imageName" TEXT NOT NULL,
|
||||
"originalFile" TEXT NOT NULL,
|
||||
"uploadDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"altText" TEXT,
|
||||
"description" TEXT,
|
||||
"imageData" TEXT,
|
||||
"creationMonth" INTEGER,
|
||||
"creationYear" INTEGER,
|
||||
"creationDate" TIMESTAMP(3),
|
||||
"albumId" TEXT,
|
||||
"artistId" TEXT,
|
||||
|
||||
CONSTRAINT "Image_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Image" ADD CONSTRAINT "Image_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "Album"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Image" ADD CONSTRAINT "Image_artistId_fkey" FOREIGN KEY ("artistId") REFERENCES "Artist"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -0,0 +1,47 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "ImageMetadata" (
|
||||
"id" TEXT NOT NULL,
|
||||
"imageId" TEXT NOT NULL,
|
||||
"format" TEXT NOT NULL,
|
||||
"width" INTEGER NOT NULL,
|
||||
"height" INTEGER NOT NULL,
|
||||
"space" TEXT NOT NULL,
|
||||
"channels" INTEGER NOT NULL,
|
||||
"depth" TEXT NOT NULL,
|
||||
"density" INTEGER,
|
||||
"bitsPerSample" INTEGER,
|
||||
"isProgressive" BOOLEAN,
|
||||
"isPalette" BOOLEAN,
|
||||
"hasProfile" BOOLEAN,
|
||||
"hasAlpha" BOOLEAN,
|
||||
"autoOrientW" INTEGER,
|
||||
"autoOrientH" INTEGER,
|
||||
|
||||
CONSTRAINT "ImageMetadata_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ImageStats" (
|
||||
"id" TEXT NOT NULL,
|
||||
"imageId" TEXT NOT NULL,
|
||||
"isOpaque" BOOLEAN NOT NULL,
|
||||
"entropy" DOUBLE PRECISION NOT NULL,
|
||||
"sharpness" DOUBLE PRECISION NOT NULL,
|
||||
"dominantR" INTEGER NOT NULL,
|
||||
"dominantG" INTEGER NOT NULL,
|
||||
"dominantB" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "ImageStats_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ImageMetadata_imageId_key" ON "ImageMetadata"("imageId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ImageStats_imageId_key" ON "ImageStats"("imageId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ImageMetadata" ADD CONSTRAINT "ImageMetadata_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ImageStats" ADD CONSTRAINT "ImageStats_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
38
prisma/migrations/20250626184532_image_variant/migration.sql
Normal file
38
prisma/migrations/20250626184532_image_variant/migration.sql
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `updatedAt` to the `ImageMetadata` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `updatedAt` to the `ImageStats` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "ImageMetadata" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ImageStats" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ImageVariant" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"imageId" TEXT NOT NULL,
|
||||
"s3Key" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"height" INTEGER NOT NULL,
|
||||
"width" INTEGER NOT NULL,
|
||||
"fileExtension" TEXT,
|
||||
"mimeType" TEXT,
|
||||
"url" TEXT,
|
||||
"sizeBytes" INTEGER,
|
||||
|
||||
CONSTRAINT "ImageVariant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ImageVariant_imageId_key" ON "ImageVariant"("imageId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ImageVariant" ADD CONSTRAINT "ImageVariant_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
3
prisma/migrations/20250626184958_image/migration.sql
Normal file
3
prisma/migrations/20250626184958_image/migration.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "fileSize" INTEGER,
|
||||
ADD COLUMN "fileType" TEXT;
|
42
prisma/migrations/20250626190045_color_palette/migration.sql
Normal file
42
prisma/migrations/20250626190045_color_palette/migration.sql
Normal file
@ -0,0 +1,42 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "ColorPalette" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"name" TEXT,
|
||||
"type" TEXT,
|
||||
|
||||
CONSTRAINT "ColorPalette_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ColorPaletteItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"tone" INTEGER,
|
||||
"hex" TEXT,
|
||||
"colorPaletteId" TEXT,
|
||||
|
||||
CONSTRAINT "ColorPaletteItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_ImagePalettes" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_ImagePalettes_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_ImagePalettes_B_index" ON "_ImagePalettes"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ColorPaletteItem" ADD CONSTRAINT "ColorPaletteItem_colorPaletteId_fkey" FOREIGN KEY ("colorPaletteId") REFERENCES "ColorPalette"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ImagePalettes" ADD CONSTRAINT "_ImagePalettes_A_fkey" FOREIGN KEY ("A") REFERENCES "ColorPalette"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ImagePalettes" ADD CONSTRAINT "_ImagePalettes_B_fkey" FOREIGN KEY ("B") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
78
prisma/migrations/20250626191041_colors/migration.sql
Normal file
78
prisma/migrations/20250626191041_colors/migration.sql
Normal file
@ -0,0 +1,78 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "ImageMetadata_imageId_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "ImageStats_imageId_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "ImageVariant_imageId_key";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ExtractColor" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"hex" TEXT NOT NULL,
|
||||
"imageId" TEXT NOT NULL,
|
||||
"blue" INTEGER NOT NULL,
|
||||
"green" INTEGER NOT NULL,
|
||||
"red" INTEGER NOT NULL,
|
||||
"isLight" BOOLEAN NOT NULL,
|
||||
"area" DOUBLE PRECISION,
|
||||
"hue" DOUBLE PRECISION,
|
||||
"saturation" DOUBLE PRECISION,
|
||||
"value" DOUBLE PRECISION,
|
||||
|
||||
CONSTRAINT "ExtractColor_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ImageColor" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"imageId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"hex" TEXT,
|
||||
"blue" INTEGER,
|
||||
"green" INTEGER,
|
||||
"red" INTEGER,
|
||||
|
||||
CONSTRAINT "ImageColor_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ThemeSeed" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"imageId" TEXT NOT NULL,
|
||||
"seedHex" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "ThemeSeed_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PixelSummary" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"imageId" TEXT NOT NULL,
|
||||
"channels" INTEGER NOT NULL,
|
||||
"height" INTEGER NOT NULL,
|
||||
"width" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "PixelSummary_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ExtractColor" ADD CONSTRAINT "ExtractColor_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT 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 "ThemeSeed" ADD CONSTRAINT "ThemeSeed_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PixelSummary" ADD CONSTRAINT "PixelSummary_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,10 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `isLight` on the `ExtractColor` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `value` on the `ExtractColor` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "ExtractColor" DROP COLUMN "isLight",
|
||||
DROP COLUMN "value";
|
@ -19,8 +19,8 @@ model Gallery {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String
|
||||
slug String @unique
|
||||
name String
|
||||
|
||||
description String?
|
||||
|
||||
@ -45,7 +45,7 @@ model Album {
|
||||
// coverImage Image? @relation("AlbumCoverImage", fields: [coverImageId], references: [id])
|
||||
gallery Gallery? @relation(fields: [galleryId], references: [id])
|
||||
|
||||
// images Image[]
|
||||
images Image[]
|
||||
}
|
||||
|
||||
model Artist {
|
||||
@ -53,16 +53,13 @@ model Artist {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
displayName String
|
||||
slug String @unique
|
||||
displayName String
|
||||
|
||||
nickname String?
|
||||
// mentionUrl String?
|
||||
// contact String?
|
||||
// urls String[]
|
||||
|
||||
socials Social[]
|
||||
// images Image[]
|
||||
images Image[]
|
||||
}
|
||||
|
||||
model Social {
|
||||
@ -70,8 +67,8 @@ model Social {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
platform String
|
||||
handle String
|
||||
platform String
|
||||
isPrimary Boolean
|
||||
|
||||
link String?
|
||||
@ -79,3 +76,190 @@ model Social {
|
||||
artistId String?
|
||||
artist Artist? @relation(fields: [artistId], references: [id])
|
||||
}
|
||||
|
||||
model Image {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
fileKey String
|
||||
imageName String
|
||||
originalFile String
|
||||
uploadDate DateTime @default(now())
|
||||
|
||||
altText String?
|
||||
description String?
|
||||
fileType String?
|
||||
imageData String?
|
||||
creationMonth Int?
|
||||
creationYear Int?
|
||||
fileSize Int?
|
||||
creationDate DateTime?
|
||||
|
||||
albumId String?
|
||||
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])
|
||||
|
||||
colors ImageColor[]
|
||||
extractColors ExtractColor[]
|
||||
metadata ImageMetadata[]
|
||||
pixels PixelSummary[]
|
||||
stats ImageStats[]
|
||||
theme ThemeSeed[]
|
||||
variants ImageVariant[]
|
||||
// colors ImageColor[]
|
||||
// extractColors ExtractColor[]
|
||||
//
|
||||
// albumCover Album[] @relation("AlbumCoverImage")
|
||||
// categories Category[] @relation("ImageCategories")
|
||||
// galleryCover Gallery[] @relation("GalleryCoverImage")
|
||||
palettes ColorPalette[] @relation("ImagePalettes")
|
||||
// tags Tag[] @relation("ImageTags")
|
||||
}
|
||||
|
||||
model ImageMetadata {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
depth String
|
||||
format String
|
||||
space String
|
||||
channels Int
|
||||
height Int
|
||||
width Int
|
||||
|
||||
autoOrientH Int?
|
||||
autoOrientW Int?
|
||||
bitsPerSample Int?
|
||||
density Int?
|
||||
hasAlpha Boolean?
|
||||
hasProfile Boolean?
|
||||
isPalette Boolean?
|
||||
isProgressive Boolean?
|
||||
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
model ImageStats {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
entropy Float
|
||||
sharpness Float
|
||||
dominantB Int
|
||||
dominantG Int
|
||||
dominantR Int
|
||||
isOpaque Boolean
|
||||
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
model ImageVariant {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
s3Key String
|
||||
type String
|
||||
height Int
|
||||
width Int
|
||||
|
||||
fileExtension String?
|
||||
mimeType String?
|
||||
url String?
|
||||
sizeBytes Int?
|
||||
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
model ColorPalette {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String?
|
||||
type String?
|
||||
|
||||
items ColorPaletteItem[]
|
||||
images Image[] @relation("ImagePalettes")
|
||||
}
|
||||
|
||||
model ColorPaletteItem {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tone Int?
|
||||
hex String?
|
||||
|
||||
colorPaletteId String?
|
||||
colorPalette ColorPalette? @relation(fields: [colorPaletteId], references: [id])
|
||||
}
|
||||
|
||||
model ExtractColor {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
hex String
|
||||
imageId String
|
||||
blue Int
|
||||
green Int
|
||||
red Int
|
||||
// isLight Boolean
|
||||
|
||||
area Float?
|
||||
hue Float?
|
||||
saturation Float?
|
||||
// value Float?
|
||||
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
model ImageColor {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
type String
|
||||
|
||||
hex String?
|
||||
blue Int?
|
||||
green Int?
|
||||
red Int?
|
||||
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
286
public/watermark/fellies-watermark.svg
Normal file
286
public/watermark/fellies-watermark.svg
Normal file
@ -0,0 +1,286 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000"
|
||||
preserveAspectRatio="xMidYMid meet1" opacity="0.5">
|
||||
|
||||
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M820 7431 c-99 -32 -175 -101 -225 -206 -46 -95 -65 -171 -72 -290
|
||||
-7 -131 13 -229 73 -353 34 -71 47 -112 55 -173 5 -45 28 -130 52 -197 38
|
||||
-107 41 -125 40 -207 -1 -49 -11 -137 -23 -195 -27 -132 -37 -323 -21 -416 30
|
||||
-176 105 -296 325 -522 50 -51 92 -94 93 -95 1 -1 -105 -2 -235 -2 -229 0
|
||||
-318 -7 -308 -24 3 -5 103 -11 223 -14 120 -4 254 -13 298 -20 77 -14 81 -16
|
||||
127 -67 27 -30 48 -56 48 -59 0 -3 -17 -7 -38 -9 -144 -10 -592 -180 -592
|
||||
-225 0 -4 12 -2 27 4 185 73 450 148 579 165 l52 6 37 -58 c21 -33 38 -65 39
|
||||
-72 1 -6 -34 -27 -77 -46 -116 -51 -264 -155 -356 -251 -45 -47 -81 -90 -81
|
||||
-96 0 -13 6 -9 116 80 129 105 285 198 398 237 l40 15 71 -69 c38 -38 99 -85
|
||||
135 -105 53 -28 69 -44 86 -79 48 -98 138 -169 269 -213 140 -47 352 -30 467
|
||||
38 69 40 134 106 171 172 29 53 41 65 102 98 40 21 95 63 128 97 l58 59 44
|
||||
-14 c102 -30 289 -145 428 -262 49 -41 90 -73 92 -71 10 10 -69 110 -127 163
|
||||
-99 90 -236 171 -356 210 -29 10 -52 20 -52 24 0 4 19 36 43 71 l42 65 45 -3
|
||||
c115 -6 356 -75 527 -151 87 -38 93 -38 62 -5 -28 31 -164 101 -267 138 -76
|
||||
27 -276 76 -311 76 -32 0 -26 17 28 72 l48 51 94 13 c52 8 159 16 239 19 80 3
|
||||
170 8 200 11 47 5 51 8 35 17 -24 14 -408 20 -472 8 -23 -5 -45 -6 -48 -3 -3
|
||||
3 44 55 104 116 207 209 285 328 315 478 26 124 18 249 -30 473 -9 44 -16 118
|
||||
-16 165 0 71 6 102 37 190 28 81 38 130 44 211 8 96 12 112 44 167 68 115 93
|
||||
233 83 381 -11 171 -59 302 -143 392 -65 70 -112 92 -193 93 -94 1 -240 -52
|
||||
-364 -132 -93 -60 -247 -185 -295 -241 l-51 -59 -83 18 c-150 34 -277 44 -507
|
||||
43 -196 -1 -246 -4 -387 -28 l-162 -28 -27 32 c-84 100 -230 218 -369 298
|
||||
-119 69 -308 116 -375 94z m149 -146 c108 -38 249 -165 388 -347 101 -134 145
|
||||
-219 141 -272 -3 -39 -1 -41 27 -44 76 -7 141 85 133 188 l-4 59 50 10 c105
|
||||
20 108 20 100 2 -13 -32 -20 -127 -11 -166 11 -53 33 -75 73 -75 25 0 38 7 55
|
||||
31 26 37 37 141 20 190 -6 18 -10 34 -8 36 2 1 31 6 65 9 l62 7 0 -73 c0 -83
|
||||
27 -154 61 -165 31 -10 77 14 101 53 18 29 22 50 21 115 l0 79 30 -6 c16 -3
|
||||
47 -6 69 -6 34 0 39 -3 33 -17 -29 -75 -21 -190 16 -229 34 -36 82 -32 111 9
|
||||
19 26 23 45 23 102 0 39 -4 80 -8 93 -8 21 -6 22 45 22 95 0 98 -3 91 -70 -7
|
||||
-74 21 -139 78 -177 52 -34 75 -25 84 32 23 155 304 503 474 588 92 46 168 51
|
||||
222 14 85 -57 137 -176 146 -332 7 -120 -8 -187 -70 -304 -47 -91 -51 -102
|
||||
-59 -201 -7 -77 -18 -132 -44 -205 -60 -174 -63 -317 -11 -520 9 -33 19 -96
|
||||
23 -140 l7 -80 -74 76 c-123 126 -219 161 -338 123 -49 -16 -53 -16 -128 6
|
||||
-92 27 -210 24 -284 -8 -94 -41 -162 -158 -211 -361 -46 -191 -33 -276 56
|
||||
-368 41 -42 59 -53 84 -53 21 0 35 6 42 19 13 25 13 68 0 76 -19 12 -31 82
|
||||
-21 126 18 81 49 106 174 140 134 37 200 95 242 212 l23 62 50 3 c62 4 153
|
||||
-22 191 -53 28 -24 70 -117 72 -158 0 -13 3 -16 6 -9 4 10 11 8 29 -9 31 -29
|
||||
30 -41 -5 -114 -24 -51 -45 -75 -140 -154 -61 -52 -111 -97 -111 -100 0 -10
|
||||
-180 -225 -197 -234 -10 -6 -21 -2 -35 13 -58 62 -151 97 -176 67 -11 -13 -7
|
||||
-19 57 -78 27 -25 16 -39 -31 -39 -30 0 -68 -31 -68 -55 0 -21 34 -36 68 -29
|
||||
33 7 72 -10 72 -30 0 -23 -40 -50 -83 -55 -54 -8 -62 -35 -21 -68 l29 -23 -19
|
||||
-34 c-20 -38 -64 -62 -97 -53 -15 4 -19 2 -15 -9 4 -9 -2 -14 -16 -14 -12 0
|
||||
-19 -3 -15 -6 3 -3 22 -6 41 -6 l36 0 -22 -17 c-46 -35 -120 -56 -198 -55 -97
|
||||
1 -234 46 -263 86 -29 39 -15 61 79 133 83 63 166 157 192 215 21 49 21 176
|
||||
-1 217 -31 58 -83 52 -89 -9 -2 -22 -9 -32 -23 -35 -42 -7 -254 -8 -335 -1
|
||||
l-85 7 -5 38 c-4 32 -9 38 -30 41 -41 5 -71 -36 -79 -107 -17 -139 58 -264
|
||||
224 -376 50 -34 61 -46 63 -74 5 -53 -19 -77 -105 -105 -116 -36 -178 -41
|
||||
-252 -20 -35 9 -65 23 -68 31 -3 7 -11 13 -19 13 -8 0 -14 5 -14 10 0 6 10 8
|
||||
24 4 14 -3 33 -1 43 4 16 10 14 13 -19 26 -21 9 -51 16 -69 16 -22 0 -41 10
|
||||
-67 36 l-36 36 32 25 c50 39 34 93 -24 79 -38 -10 -76 18 -72 52 3 25 7 27 62
|
||||
32 73 6 95 30 58 60 -13 11 -39 22 -58 26 -38 7 -44 25 -15 45 23 17 34 49 22
|
||||
68 -6 10 -20 11 -51 6 -32 -5 -53 -18 -86 -51 -23 -24 -47 -44 -52 -44 -5 0
|
||||
-29 26 -53 58 -88 116 -181 219 -263 289 -82 70 -135 143 -150 204 -5 23 1 32
|
||||
37 62 33 27 47 48 60 91 14 44 26 62 55 81 53 34 126 57 175 53 42 -3 42 -4
|
||||
60 -59 20 -63 71 -129 128 -164 20 -14 79 -36 131 -50 161 -46 187 -92 151
|
||||
-276 -12 -60 -1 -89 32 -89 43 0 80 26 118 82 54 79 65 149 43 269 -45 248
|
||||
-107 368 -220 427 -45 23 -63 26 -156 26 -77 1 -120 -4 -163 -18 -56 -18 -61
|
||||
-18 -105 -3 -117 42 -216 5 -338 -126 l-76 -82 6 95 c3 52 15 140 28 195 16
|
||||
74 22 132 22 225 0 124 -1 126 -46 245 -33 86 -48 143 -53 199 -6 58 -15 93
|
||||
-38 135 -16 31 -40 76 -53 101 -65 125 -45 382 39 514 59 93 148 126 246 91z
|
||||
m491 -1714 c0 -76 24 -125 72 -149 39 -18 101 8 122 53 9 18 16 38 16 44 0 24
|
||||
22 9 36 -25 19 -44 5 -94 -33 -124 -50 -39 -171 -19 -248 43 -38 30 -85 118
|
||||
-85 159 0 24 7 32 43 48 72 33 77 30 77 -49z m1486 44 c19 -12 24 -24 24 -56
|
||||
0 -126 -160 -246 -280 -210 -68 20 -97 89 -65 151 20 39 31 38 39 -5 7 -37 52
|
||||
-75 89 -75 67 0 111 73 100 167 l-6 56 37 -6 c21 -3 49 -13 62 -22z m-556
|
||||
-900 c22 -23 1 -48 -50 -61 -51 -12 -70 -35 -54 -62 26 -41 -41 -112 -107
|
||||
-112 -59 0 -137 54 -144 100 -9 50 -18 64 -53 74 -36 10 -48 36 -33 65 11 19
|
||||
20 20 218 15 167 -3 211 -7 223 -19z m-95 -545 c55 -16 121 -35 147 -41 58
|
||||
-13 55 -25 -20 -79 -129 -93 -319 -106 -467 -33 -61 30 -119 78 -111 91 6 11
|
||||
120 50 216 76 82 21 125 19 235 -14z"/>
|
||||
<path d="M890 7116 c0 -3 9 -10 20 -16 11 -6 20 -8 20 -6 0 3 -9 10 -20 16
|
||||
-11 6 -20 8 -20 6z"/>
|
||||
<path d="M3410 7109 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M3295 7070 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M1031 7066 c10 -9 69 -36 69 -32 0 6 -55 36 -66 36 -4 0 -5 -2 -3 -4z"/>
|
||||
<path d="M3299 7013 l-24 -28 28 24 c25 23 32 31 24 31 -2 0 -14 -12 -28 -27z"/>
|
||||
<path d="M1125 7014 c6 -5 23 -20 40 -34 29 -24 29 -24 10 -2 -11 13 -29 28
|
||||
-40 34 -11 6 -15 6 -10 2z"/>
|
||||
<path d="M3158 6989 l-33 -31 38 27 c20 15 37 29 37 31 0 10 -12 2 -42 -27z"/>
|
||||
<path d="M3251 6957 c-13 -21 -12 -21 5 -5 10 10 16 20 13 22 -3 3 -11 -5 -18
|
||||
-17z"/>
|
||||
<path d="M865 6950 c3 -5 8 -10 11 -10 2 0 4 5 4 10 0 6 -5 10 -11 10 -5 0 -7
|
||||
-4 -4 -10z"/>
|
||||
<path d="M1265 6878 c38 -43 70 -78 73 -78 7 0 -74 93 -109 125 -18 17 -2 -5
|
||||
36 -47z"/>
|
||||
<path d="M3105 6940 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
|
||||
-8 -4 -11 -10z"/>
|
||||
<path d="M3295 6920 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M3384 6908 l-19 -23 23 19 c21 18 27 26 19 26 -2 0 -12 -10 -23 -22z"/>
|
||||
<path d="M3039 6863 c-25 -27 -74 -82 -108 -123 -71 -83 -25 -38 87 88 81 90
|
||||
92 109 21 35z"/>
|
||||
<path d="M990 6855 c13 -14 26 -25 28 -25 3 0 -5 11 -18 25 -13 14 -26 25 -28
|
||||
25 -3 0 5 -11 18 -25z"/>
|
||||
<path d="M3304 6838 l-19 -23 23 19 c21 18 27 26 19 26 -2 0 -12 -10 -23 -22z"/>
|
||||
<path d="M1040 6806 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
|
||||
13z"/>
|
||||
<path d="M1356 6770 c23 -33 117 -140 120 -137 3 2 -16 26 -40 53 -69 78 -92
|
||||
101 -80 84z"/>
|
||||
<path d="M893 6773 c15 -2 37 -2 50 0 12 2 0 4 -28 4 -27 0 -38 -2 -22 -4z"/>
|
||||
<path d="M3388 6773 c6 -2 18 -2 25 0 6 3 1 5 -13 5 -14 0 -19 -2 -12 -5z"/>
|
||||
<path d="M3410 6742 c-35 -34 -35 -35 -4 -14 33 23 62 53 48 51 -5 -1 -25 -17
|
||||
-44 -37z"/>
|
||||
<path d="M904 6741 c3 -5 22 -18 42 -30 19 -12 33 -17 30 -12 -3 5 -22 18 -42
|
||||
30 -19 12 -33 17 -30 12z"/>
|
||||
<path d="M983 6583 c-7 -2 -13 -9 -13 -15 0 -7 4 -7 13 0 7 6 35 12 62 15 l50
|
||||
3 -50 1 c-27 1 -56 -1 -62 -4z"/>
|
||||
<path d="M1773 6582 c-44 -29 -63 -211 -29 -287 23 -54 58 -105 73 -105 22 0
|
||||
46 109 60 275 4 59 2 74 -13 92 -20 25 -71 39 -91 25z"/>
|
||||
<path d="M2262 6581 c-31 -19 -41 -181 -13 -214 23 -28 51 -21 67 18 35 85 -4
|
||||
227 -54 196z"/>
|
||||
<path d="M2456 6559 c-14 -17 -26 -40 -26 -53 0 -79 34 -259 57 -299 10 -16
|
||||
43 16 67 66 33 67 42 136 27 209 -22 105 -75 137 -125 77z"/>
|
||||
<path d="M2001 6549 c-15 -43 -14 -148 2 -178 15 -30 28 -35 51 -18 24 18 33
|
||||
127 14 182 -17 53 -51 60 -67 14z"/>
|
||||
<path d="M3270 6495 c-24 -25 -42 -45 -39 -45 3 0 25 20 49 45 24 25 42 45 39
|
||||
45 -3 0 -25 -20 -49 -45z"/>
|
||||
<path d="M1017 6499 c12 -12 24 -21 27 -18 2 2 -8 13 -22 23 l-27 19 22 -24z"/>
|
||||
<path d="M1045 6352 c-38 -43 -80 -99 -58 -77 45 45 105 116 102 119 -2 2 -22
|
||||
-17 -44 -42z"/>
|
||||
<path d="M3240 6373 c0 -5 12 -20 28 -34 l27 -24 -25 30 c-14 17 -26 32 -27
|
||||
34 -2 2 -3 0 -3 -6z"/>
|
||||
<path d="M3300 6303 c0 -4 17 -26 38 -48 39 -43 34 -33 -11 23 -15 19 -27 30
|
||||
-27 25z"/>
|
||||
<path d="M3013 6254 c-12 -31 25 -84 72 -105 51 -23 65 -19 65 18 0 35 -69
|
||||
103 -104 103 -15 0 -29 -7 -33 -16z"/>
|
||||
<path d="M1215 6242 c-41 -26 -57 -47 -57 -78 0 -41 15 -48 58 -26 61 31 89
|
||||
68 85 110 -2 18 -52 15 -86 -6z"/>
|
||||
<path d="M1972 6211 c-19 -12 -33 -81 -21 -103 15 -28 46 -22 64 12 19 37 19
|
||||
64 -1 84 -18 18 -24 19 -42 7z"/>
|
||||
<path d="M2285 6200 c-8 -24 10 -84 31 -101 20 -16 54 15 54 50 0 58 -70 100
|
||||
-85 51z"/>
|
||||
<path d="M905 6150 c-9 -16 -13 -30 -11 -30 3 0 12 14 21 30 9 17 13 30 11 30
|
||||
-3 0 -12 -13 -21 -30z"/>
|
||||
<path d="M2604 6160 c-11 -4 -29 -25 -38 -45 -16 -33 -17 -55 -10 -194 4 -86
|
||||
6 -165 5 -176 -2 -11 -22 -39 -46 -63 -82 -83 -126 -200 -124 -327 2 -70 2
|
||||
-70 9 25 13 157 59 250 170 337 30 24 67 54 81 67 60 55 83 244 41 339 -22 49
|
||||
-40 57 -88 37z"/>
|
||||
<path d="M3400 6163 c0 -6 8 -17 18 -24 16 -13 16 -13 2 6 -8 11 -16 22 -17
|
||||
24 -2 2 -3 0 -3 -6z"/>
|
||||
<path d="M1636 6138 c-37 -58 -28 -209 18 -308 22 -46 88 -110 116 -110 8 0
|
||||
63 -65 98 -116 7 -11 6 -7 -2 11 -8 17 -32 51 -55 77 l-41 46 12 120 c13 135
|
||||
4 205 -35 264 -30 43 -88 52 -111 16z"/>
|
||||
<path d="M2156 5995 c-10 -28 -7 -70 8 -90 29 -40 69 18 50 74 -11 30 -48 40
|
||||
-58 16z"/>
|
||||
<path d="M2814 5995 c-9 -22 28 -79 58 -89 20 -7 33 -5 52 8 36 23 35 61 -4
|
||||
81 -38 20 -99 20 -106 0z"/>
|
||||
<path d="M1398 5989 c-57 -21 -40 -81 27 -95 22 -4 38 0 58 16 65 52 2 110
|
||||
-85 79z"/>
|
||||
<path d="M1937 5972 c-9 -10 -20 -32 -23 -49 -5 -25 -2 -33 16 -43 27 -15 45
|
||||
-1 60 46 18 55 -18 85 -53 46z"/>
|
||||
<path d="M2347 5984 c-12 -12 -8 -79 5 -92 35 -35 71 12 52 67 -9 25 -42 40
|
||||
-57 25z"/>
|
||||
<path d="M1024 5933 c-30 -31 -54 -82 -54 -113 0 -46 85 -1 110 59 28 68 -9
|
||||
104 -56 54z"/>
|
||||
<path d="M3234 5946 c-17 -45 54 -154 91 -140 18 7 20 56 4 87 -30 57 -82 86
|
||||
-95 53z"/>
|
||||
<path d="M1453 5833 c15 -2 37 -2 50 0 12 2 0 4 -28 4 -27 0 -38 -2 -22 -4z"/>
|
||||
<path d="M2843 5833 c9 -2 25 -2 35 0 9 3 1 5 -18 5 -19 0 -27 -2 -17 -5z"/>
|
||||
<path d="M2773 5823 c9 -2 25 -2 35 0 9 3 1 5 -18 5 -19 0 -27 -2 -17 -5z"/>
|
||||
<path d="M1605 5810 c3 -4 16 -10 30 -12 13 -3 22 -2 20 2 -3 4 -16 10 -30 12
|
||||
-13 3 -22 2 -20 -2z"/>
|
||||
<path d="M2728 5813 c7 -3 16 -2 19 1 4 3 -2 6 -13 5 -11 0 -14 -3 -6 -6z"/>
|
||||
<path d="M2685 5800 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M3010 5796 c0 -2 7 -7 16 -10 8 -3 12 -2 9 4 -6 10 -25 14 -25 6z"/>
|
||||
<path d="M1249 5753 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M1901 5535 c1 -19 18 -51 18 -35 0 8 -4 22 -9 30 -5 8 -9 11 -9 5z"/>
|
||||
<path d="M1922 5450 c0 -19 2 -27 5 -17 2 9 2 25 0 35 -3 9 -5 1 -5 -18z"/>
|
||||
<path d="M1159 5424 c19 -39 86 -112 150 -163 35 -28 65 -51 67 -51 10 0 -1
|
||||
11 -63 60 -60 46 -82 69 -149 155 -15 20 -15 20 -5 -1z"/>
|
||||
<path d="M1932 5352 c-1 -35 -9 -77 -18 -93 -8 -17 -14 -35 -12 -41 10 -29 39
|
||||
84 35 139 l-3 58 -2 -63z"/>
|
||||
<path d="M3130 5365 c-7 -9 -8 -15 -2 -15 5 0 12 7 16 15 3 8 4 15 2 15 -2 0
|
||||
-9 -7 -16 -15z"/>
|
||||
<path d="M3094 5318 l-19 -23 23 19 c12 11 22 21 22 23 0 8 -8 2 -26 -19z"/>
|
||||
<path d="M1082 5298 c-16 -16 -15 -71 2 -113 16 -38 79 -77 95 -59 23 22 -36
|
||||
175 -69 182 -9 1 -21 -3 -28 -10z"/>
|
||||
<path d="M3215 5302 c-14 -10 -55 -113 -55 -137 0 -45 16 -50 60 -21 43 28 60
|
||||
58 60 106 0 43 -37 73 -65 52z"/>
|
||||
<path d="M3020 5258 c-26 -24 -32 -32 -15 -20 28 18 73 63 64 62 -2 -1 -24
|
||||
-19 -49 -42z"/>
|
||||
<path d="M2410 5241 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
|
||||
<path d="M1390 5205 c0 -6 90 -58 95 -54 2 2 -19 16 -46 32 -27 15 -49 25 -49
|
||||
22z"/>
|
||||
<path d="M2858 5153 c-27 -14 -46 -28 -45 -30 5 -4 97 47 97 53 0 6 -1 5 -52
|
||||
-23z"/>
|
||||
<path d="M3214 5118 l-19 -23 23 19 c21 18 27 26 19 26 -2 0 -12 -10 -23 -22z"/>
|
||||
<path d="M1080 5126 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
|
||||
13z"/>
|
||||
<path d="M1620 5086 c0 -3 9 -10 20 -16 11 -6 20 -8 20 -6 0 3 -9 10 -20 16
|
||||
-11 6 -20 8 -20 6z"/>
|
||||
<path d="M1322 5058 c-7 -7 -12 -29 -12 -49 0 -31 5 -40 32 -57 17 -11 37 -18
|
||||
45 -15 19 8 16 63 -7 101 -21 34 -38 40 -58 20z"/>
|
||||
<path d="M2977 5052 c-9 -10 -22 -33 -28 -52 -10 -28 -10 -35 5 -46 25 -17 58
|
||||
-4 75 31 12 25 12 32 0 54 -18 32 -32 36 -52 13z"/>
|
||||
<path d="M1170 5037 c14 -16 34 -33 45 -39 21 -11 20 -10 -35 37 l-35 30 25
|
||||
-28z"/>
|
||||
<path d="M3139 5033 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M3046 4955 c-11 -8 -15 -15 -9 -15 6 0 16 7 23 15 16 19 11 19 -14 0z"/>
|
||||
<path d="M1330 4905 c13 -14 26 -25 28 -25 3 0 -5 11 -18 25 -13 14 -26 25
|
||||
-28 25 -3 0 5 -11 18 -25z"/>
|
||||
<path d="M2999 4913 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M2870 4830 c-9 -6 -10 -10 -3 -10 6 0 15 5 18 10 8 12 4 12 -15 0z"/>
|
||||
<path d="M2683 4724 c-18 -8 -33 -16 -33 -19 0 -7 60 16 66 26 6 11 7 11 -33
|
||||
-7z"/>
|
||||
<path d="M2595 4685 c-16 -8 -23 -14 -14 -15 8 0 24 7 35 15 23 18 18 18 -21
|
||||
0z"/>
|
||||
<path d="M1695 4490 c-9 -15 3 -47 21 -54 17 -6 27 13 22 41 -4 20 -33 30 -43
|
||||
13z"/>
|
||||
<path d="M2587 4493 c-12 -11 -7 -40 9 -53 12 -11 18 -11 30 2 13 12 14 20 5
|
||||
37 -12 21 -31 27 -44 14z"/>
|
||||
<path d="M7290 6818 c-112 -31 -169 -157 -121 -263 31 -69 105 -115 181 -115
|
||||
32 0 108 35 137 62 31 29 57 111 50 160 -8 61 -50 116 -110 144 -56 26 -82 28
|
||||
-137 12z"/>
|
||||
<path d="M4562 6810 c-108 -23 -223 -123 -262 -228 -16 -41 -23 -89 -27 -174
|
||||
l-6 -118 -66 0 c-102 0 -111 -10 -111 -124 0 -58 4 -96 12 -104 7 -7 43 -12
|
||||
90 -12 l78 0 0 -434 c0 -481 -3 -459 65 -472 61 -11 213 -1 230 16 13 13 15
|
||||
75 15 453 l1 437 132 0 c172 0 170 -2 175 123 4 90 3 96 -18 107 -14 7 -59 11
|
||||
-119 10 -53 -1 -114 -2 -135 -1 l-39 1 6 81 c4 45 14 98 24 118 33 68 114 87
|
||||
235 55 34 -8 65 -11 69 -7 5 5 14 53 21 107 12 98 12 98 -11 117 -13 10 -46
|
||||
26 -74 35 -62 18 -225 27 -285 14z"/>
|
||||
<path d="M6068 6800 c-15 -9 -16 -73 -11 -817 4 -679 8 -810 20 -823 18 -20
|
||||
264 -20 288 0 13 11 15 109 15 816 l0 803 -22 15 c-18 13 -50 16 -148 16 -69
|
||||
0 -132 -5 -142 -10z"/>
|
||||
<path d="M6626 6797 c-16 -12 -17 -60 -16 -810 0 -438 3 -804 6 -812 8 -21 68
|
||||
-35 144 -35 64 1 146 12 156 23 3 2 5 368 5 813 1 762 0 809 -17 821 -25 18
|
||||
-253 18 -278 0z"/>
|
||||
<path d="M5314 6305 c-176 -38 -310 -164 -371 -348 -23 -73 -26 -98 -27 -227
|
||||
-1 -213 34 -325 137 -436 73 -79 167 -129 280 -151 146 -28 362 -2 478 58 33
|
||||
16 39 24 39 52 0 39 -34 165 -48 179 -7 7 -37 -1 -94 -23 -76 -30 -92 -33
|
||||
-193 -34 -97 0 -115 3 -154 23 -76 41 -127 113 -133 188 l-3 39 325 5 c179 3
|
||||
326 7 327 8 1 1 7 21 14 45 13 47 6 201 -13 276 -47 193 -186 324 -373 351
|
||||
-88 12 -115 12 -191 -5z m217 -240 c48 -31 80 -98 87 -181 l5 -64 -200 0 -199
|
||||
0 7 64 c11 112 63 187 143 207 47 11 117 0 157 -26z"/>
|
||||
<path d="M8061 6304 c-178 -38 -302 -155 -368 -346 -31 -90 -43 -285 -24 -392
|
||||
28 -158 94 -270 205 -344 106 -71 170 -87 346 -87 128 1 156 4 240 29 52 15
|
||||
106 34 119 43 22 14 23 20 18 72 -10 87 -35 161 -55 161 -9 0 -53 -14 -97 -31
|
||||
-235 -92 -440 -14 -471 179 l-7 42 316 0 c266 0 318 2 330 15 37 36 30 250
|
||||
-11 368 -39 111 -121 208 -214 254 -48 24 -179 53 -230 52 -18 0 -62 -7 -97
|
||||
-15z m200 -230 c52 -26 90 -101 104 -202 l7 -52 -201 0 -201 0 0 35 c0 96 42
|
||||
184 104 217 53 29 134 30 187 2z"/>
|
||||
<path d="M9040 6295 c-200 -56 -306 -231 -256 -424 20 -77 65 -143 128 -186
|
||||
44 -31 77 -45 255 -110 104 -38 132 -79 98 -144 -20 -39 -79 -61 -163 -61 -78
|
||||
0 -121 10 -216 52 -62 27 -71 29 -81 15 -18 -25 -48 -172 -41 -200 19 -78 304
|
||||
-132 487 -93 138 30 235 101 286 211 23 51 27 72 27 142 -2 168 -86 259 -304
|
||||
333 -94 32 -148 60 -170 88 -26 33 -27 95 -1 121 44 44 178 41 288 -8 39 -17
|
||||
78 -31 87 -31 23 0 34 28 46 122 15 114 11 120 -93 154 -104 34 -289 43 -377
|
||||
19z"/>
|
||||
<path d="M7202 6274 l-22 -15 0 -544 0 -545 25 -16 c21 -14 45 -16 150 -12 79
|
||||
4 130 10 140 18 13 11 15 82 15 551 0 526 0 539 -20 559 -18 18 -33 20 -143
|
||||
20 -95 0 -128 -4 -145 -16z"/>
|
||||
<path d="M7176 4917 c-15 -11 -18 -32 -19 -156 l-2 -144 -95 2 c-131 3 -134 0
|
||||
-138 -127 -4 -92 -2 -102 18 -122 19 -19 31 -21 117 -19 l96 2 -2 -334 c-3
|
||||
-440 8 -498 112 -608 81 -84 218 -120 387 -101 96 11 146 25 174 49 16 14 18
|
||||
25 12 91 -7 87 -21 143 -39 158 -8 7 -26 8 -52 2 -115 -27 -192 -2 -225 73
|
||||
-18 38 -20 68 -20 355 l0 312 143 0 c78 0 149 5 157 10 16 10 33 175 23 218
|
||||
-8 35 -41 42 -193 42 l-139 0 6 128 c3 71 3 141 -1 155 l-6 27 -148 0 c-106 0
|
||||
-153 -4 -166 -13z"/>
|
||||
<path d="M5023 4649 c-105 -13 -250 -59 -278 -87 -23 -22 -25 -32 -25 -106 1
|
||||
-92 12 -162 29 -179 8 -8 23 -7 59 7 159 62 345 85 441 55 84 -25 131 -100
|
||||
131 -207 l0 -38 -165 4 c-186 5 -255 -4 -351 -48 -82 -37 -147 -98 -184 -174
|
||||
-108 -221 -24 -467 187 -546 50 -19 74 -21 173 -18 104 3 121 6 180 34 36 17
|
||||
86 47 111 67 l47 35 6 -49 c5 -33 13 -52 26 -59 24 -13 260 -13 294 0 l26 10
|
||||
0 417 c-1 468 -7 533 -61 640 -38 75 -109 149 -179 187 -100 55 -296 78 -467
|
||||
55z m326 -765 l31 -6 0 -98 0 -97 -39 -31 c-75 -60 -119 -77 -202 -77 -47 0
|
||||
-86 6 -103 14 -71 38 -92 163 -37 225 50 57 232 93 350 70z"/>
|
||||
<path d="M6630 4639 c-79 -15 -144 -55 -219 -134 -39 -41 -73 -75 -76 -75 -3
|
||||
0 -5 41 -5 90 0 89 0 91 -27 101 -38 15 -271 7 -301 -10 l-24 -13 4 -620 c3
|
||||
-600 4 -620 22 -634 26 -19 284 -20 308 -1 15 11 17 57 20 406 l4 394 58 53
|
||||
c71 64 139 92 255 104 48 5 90 15 98 23 15 15 42 181 43 259 0 37 -4 50 -19
|
||||
58 -23 12 -77 11 -141 -1z"/>
|
||||
<path d="M4164 3685 c-72 -32 -122 -128 -110 -210 19 -118 131 -180 261 -144
|
||||
94 26 157 150 121 242 -19 51 -68 100 -115 116 -45 15 -118 13 -157 -4z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 17 KiB |
7
src/actions/images/deleteImage.ts
Normal file
7
src/actions/images/deleteImage.ts
Normal file
@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function deleteImage(id: string) {
|
||||
await prisma.gallery.delete({ where: { id } });
|
||||
}
|
21
src/actions/images/updateImage.ts
Normal file
21
src/actions/images/updateImage.ts
Normal file
@ -0,0 +1,21 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { gallerySchema } from "@/schemas/galleries/gallerySchema";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export async function updateImage(
|
||||
values: z.infer<typeof gallerySchema>,
|
||||
id: string
|
||||
) {
|
||||
return await prisma.gallery.update({
|
||||
where: {
|
||||
id: id
|
||||
},
|
||||
data: {
|
||||
name: values.name,
|
||||
slug: values.slug,
|
||||
description: values.description,
|
||||
}
|
||||
})
|
||||
}
|
315
src/actions/images/uploadImage.ts
Normal file
315
src/actions/images/uploadImage.ts
Normal file
@ -0,0 +1,315 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { s3 } from "@/lib/s3";
|
||||
import { imageUploadSchema } from "@/schemas/images/imageSchema";
|
||||
import { VibrantSwatch } from "@/types/VibrantSwatch";
|
||||
import { extractPaletteTones, rgbToHex, upsertPalettes } from "@/utils/uploadHelper";
|
||||
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { argbFromHex, themeFromSourceColor } from "@material/material-color-utilities";
|
||||
import { extractColors } from "extract-colors";
|
||||
import getPixels from "get-pixels";
|
||||
import { NdArray } from "ndarray";
|
||||
import { Vibrant } from "node-vibrant/node";
|
||||
import path from "path";
|
||||
import sharp from "sharp";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export async function uploadImage(values: z.infer<typeof imageUploadSchema>) {
|
||||
const imageFile = values.file[0];
|
||||
const imageName = values.imageName;
|
||||
|
||||
if (!(imageFile instanceof File)) {
|
||||
console.log("No image or invalid type");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!imageName) {
|
||||
console.log("No name for the image provided");
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileName = imageFile.name;
|
||||
const fileType = imageFile.type;
|
||||
const fileSize = imageFile.size;
|
||||
const lastModified = new Date(imageFile.lastModified);
|
||||
const year = lastModified.getUTCFullYear();
|
||||
const month = lastModified.getUTCMonth() + 1;
|
||||
|
||||
const fileKey = uuidv4();
|
||||
|
||||
const arrayBuffer = await imageFile.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const imageDataUrl = `data:${imageFile.type};base64,${buffer.toString("base64")}`;
|
||||
|
||||
const originalKey = `original/${fileKey}.webp`;
|
||||
const watermarkedKey = `watermarked/${fileKey}.webp`;
|
||||
const resizedKey = `resized/${fileKey}.webp`;
|
||||
const thumbnailKey = `thumbnails/${fileKey}.webp`;
|
||||
|
||||
const sharpData = sharp(buffer);
|
||||
const metadata = await sharpData.metadata();
|
||||
const stats = await sharpData.stats();
|
||||
|
||||
const palette = await Vibrant.from(buffer).getPalette();
|
||||
|
||||
const vibrantHexes = Object.fromEntries(
|
||||
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];
|
||||
})
|
||||
);
|
||||
|
||||
const seedHex =
|
||||
vibrantHexes.Vibrant ??
|
||||
vibrantHexes.Muted ??
|
||||
vibrantHexes.DarkVibrant ??
|
||||
vibrantHexes.DarkMuted ??
|
||||
vibrantHexes.LightVibrant ??
|
||||
vibrantHexes.LightMuted ??
|
||||
"#dfffff";
|
||||
|
||||
const theme = themeFromSourceColor(argbFromHex(seedHex));
|
||||
const primaryTones = extractPaletteTones(theme.palettes.primary);
|
||||
const secondaryTones = extractPaletteTones(theme.palettes.secondary);
|
||||
const tertiaryTones = extractPaletteTones(theme.palettes.tertiary);
|
||||
const neutralTones = extractPaletteTones(theme.palettes.neutral);
|
||||
const neutralVariantTones = extractPaletteTones(theme.palettes.neutralVariant);
|
||||
const errorTones = extractPaletteTones(theme.palettes.error);
|
||||
|
||||
const pixels = await new Promise<NdArray<Uint8Array>>((resolve, reject) => {
|
||||
getPixels(imageDataUrl, 'image/' + metadata.format || "image/jpeg", (err, pixels) => {
|
||||
if (err) reject(err);
|
||||
else resolve(pixels);
|
||||
});
|
||||
});
|
||||
|
||||
const extracted = await extractColors({
|
||||
data: Array.from(pixels.data),
|
||||
width: pixels.shape[0],
|
||||
height: pixels.shape[1]
|
||||
});
|
||||
|
||||
//--- Original file
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: "felliesartapp",
|
||||
Key: originalKey,
|
||||
Body: buffer,
|
||||
ContentType: "image/" + metadata.format,
|
||||
})
|
||||
);
|
||||
//--- Watermarked file
|
||||
const watermarkPath = path.join(process.cwd(), 'public/watermark/fellies-watermark.svg');
|
||||
const watermarkWidth = Math.round(metadata.width * 0.25);
|
||||
const watermarkBuffer = await sharp(watermarkPath)
|
||||
.resize({ width: watermarkWidth })
|
||||
.png()
|
||||
.toBuffer();
|
||||
const watermarkedBuffer = await sharp(buffer)
|
||||
.composite([{ input: watermarkBuffer, gravity: 'southwest', blend: 'atop' }])
|
||||
.toFormat('webp')
|
||||
.toBuffer()
|
||||
const watermarkedMetadata = await sharp(watermarkedBuffer).metadata();
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: "felliesartapp",
|
||||
Key: watermarkedKey,
|
||||
Body: watermarkedBuffer,
|
||||
ContentType: "image/" + watermarkedMetadata.format,
|
||||
})
|
||||
);
|
||||
//--- Resized file
|
||||
const resizedWidth = Math.min(watermarkedMetadata.width || 400, 400);
|
||||
const resizedBuffer = await sharp(watermarkedBuffer)
|
||||
.resize({ width: resizedWidth, withoutEnlargement: true })
|
||||
.toFormat('webp')
|
||||
.toBuffer();
|
||||
const resizedMetadata = await sharp(resizedBuffer).metadata();
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: "felliesartapp",
|
||||
Key: resizedKey,
|
||||
Body: resizedBuffer,
|
||||
ContentType: "image/" + resizedMetadata.format,
|
||||
})
|
||||
);
|
||||
//--- Thumbnail file
|
||||
const thumbnailWidth = Math.min(watermarkedMetadata.width || 200, 200);
|
||||
const thumbnailBuffer = await sharp(watermarkedBuffer)
|
||||
.resize({ width: thumbnailWidth, withoutEnlargement: true })
|
||||
.toFormat('webp')
|
||||
.toBuffer();
|
||||
const thumbnailMetadata = await sharp(thumbnailBuffer).metadata();
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: "felliesartapp",
|
||||
Key: thumbnailKey,
|
||||
Body: thumbnailBuffer,
|
||||
ContentType: "image/" + thumbnailMetadata.format,
|
||||
})
|
||||
);
|
||||
|
||||
const image = await prisma.image.create({
|
||||
data: {
|
||||
imageName,
|
||||
fileKey,
|
||||
originalFile: fileName,
|
||||
uploadDate: new Date(),
|
||||
|
||||
creationDate: lastModified,
|
||||
creationMonth: month,
|
||||
creationYear: year,
|
||||
imageData: imageDataUrl,
|
||||
fileType: fileType,
|
||||
fileSize: fileSize,
|
||||
altText: "",
|
||||
description: "",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.imageMetadata.create({
|
||||
data: {
|
||||
imageId: image.id,
|
||||
format: metadata.format || "unknown",
|
||||
width: metadata.width || 0,
|
||||
height: metadata.height || 0,
|
||||
space: metadata.space || "unknown",
|
||||
channels: metadata.channels || 0,
|
||||
depth: metadata.depth || "unknown",
|
||||
density: metadata.density ?? undefined,
|
||||
bitsPerSample: metadata.bitsPerSample ?? undefined,
|
||||
isProgressive: metadata.isProgressive ?? undefined,
|
||||
isPalette: metadata.isPalette ?? undefined,
|
||||
hasProfile: metadata.hasProfile ?? undefined,
|
||||
hasAlpha: metadata.hasAlpha ?? undefined,
|
||||
autoOrientW: metadata.autoOrient?.width ?? undefined,
|
||||
autoOrientH: metadata.autoOrient?.height ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.imageStats.create({
|
||||
data: {
|
||||
imageId: image.id,
|
||||
isOpaque: stats.isOpaque,
|
||||
entropy: stats.entropy,
|
||||
sharpness: stats.sharpness,
|
||||
dominantR: stats.dominant.r,
|
||||
dominantG: stats.dominant.g,
|
||||
dominantB: stats.dominant.b,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.imageVariant.createMany({
|
||||
data: [
|
||||
{
|
||||
s3Key: originalKey,
|
||||
type: "original",
|
||||
height: metadata.height,
|
||||
width: metadata.width,
|
||||
fileExtension: metadata.format,
|
||||
mimeType: "image/" + metadata.format,
|
||||
sizeBytes: metadata.size,
|
||||
imageId: image.id
|
||||
},
|
||||
{
|
||||
s3Key: watermarkedKey,
|
||||
type: "watermarked",
|
||||
height: watermarkedMetadata.height,
|
||||
width: watermarkedMetadata.width,
|
||||
fileExtension: watermarkedMetadata.format,
|
||||
mimeType: "image/" + watermarkedMetadata.format,
|
||||
sizeBytes: watermarkedMetadata.size,
|
||||
imageId: image.id
|
||||
},
|
||||
{
|
||||
s3Key: resizedKey,
|
||||
type: "resized",
|
||||
height: resizedMetadata.height,
|
||||
width: resizedMetadata.width,
|
||||
fileExtension: resizedMetadata.format,
|
||||
mimeType: "image/" + resizedMetadata.format,
|
||||
sizeBytes: resizedMetadata.size,
|
||||
imageId: image.id
|
||||
},
|
||||
{
|
||||
s3Key: thumbnailKey,
|
||||
type: "thumbnail",
|
||||
height: thumbnailMetadata.height,
|
||||
width: thumbnailMetadata.width,
|
||||
fileExtension: thumbnailMetadata.format,
|
||||
mimeType: "image/" + thumbnailMetadata.format,
|
||||
sizeBytes: thumbnailMetadata.size,
|
||||
imageId: image.id
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
await upsertPalettes(primaryTones, image.id, "primary");
|
||||
await upsertPalettes(secondaryTones, image.id, "secondary");
|
||||
await upsertPalettes(tertiaryTones, image.id, "tertiary");
|
||||
await upsertPalettes(neutralTones, image.id, "neutral");
|
||||
await upsertPalettes(neutralVariantTones, image.id, "neutralVariant");
|
||||
await upsertPalettes(errorTones, image.id, "error");
|
||||
|
||||
for (const [type, hex] of Object.entries(vibrantHexes)) {
|
||||
if (!hex) continue;
|
||||
const [r, g, b] = hex.match(/\w\w/g)!.map((h) => parseInt(h, 16));
|
||||
await prisma.imageColor.create({
|
||||
data: {
|
||||
type,
|
||||
hex,
|
||||
red: r,
|
||||
green: g,
|
||||
blue: b,
|
||||
imageId: image.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const c of extracted) {
|
||||
await prisma.extractColor.create({
|
||||
data: {
|
||||
hex: c.hex,
|
||||
red: c.red,
|
||||
green: c.green,
|
||||
blue: c.blue,
|
||||
hue: c.hue,
|
||||
saturation: c.saturation,
|
||||
// value: c.value,
|
||||
area: c.area,
|
||||
// isLight: c.isLight,
|
||||
imageId: image.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.themeSeed.create({
|
||||
data: {
|
||||
seedHex,
|
||||
imageId: image.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.pixelSummary.create({
|
||||
data: {
|
||||
width: pixels.shape[0],
|
||||
height: pixels.shape[1],
|
||||
channels: pixels.shape[2],
|
||||
imageId: image.id,
|
||||
},
|
||||
});
|
||||
|
||||
return image
|
||||
// return await prisma.gallery.create({
|
||||
// data: {
|
||||
// name: values.name,
|
||||
// slug: values.slug,
|
||||
// description: values.description,
|
||||
// }
|
||||
// })
|
||||
}
|
22
src/app/images/edit/[id]/page.tsx
Normal file
22
src/app/images/edit/[id]/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import EditGalleryForm from "@/components/galleries/edit/EditGalleryForm";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function GalleriesEditPage({ params }: { params: { id: string } }) {
|
||||
const { id } = await params;
|
||||
|
||||
const gallery = await prisma.gallery.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
albums: true
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Edit gallery</h1>
|
||||
{gallery ? <EditGalleryForm gallery={gallery} /> : 'Gallery not found...'}
|
||||
</div>
|
||||
);
|
||||
}
|
20
src/app/images/page.tsx
Normal file
20
src/app/images/page.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import ListImages from "@/components/images/list/ListImages";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function ImagesPage() {
|
||||
const images = await prisma.image.findMany({ orderBy: { createdAt: "asc" } });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<h1 className="text-2xl font-bold mb-4">Images</h1>
|
||||
<Link href="/images/upload" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
|
||||
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Upload new image
|
||||
</Link>
|
||||
</div>
|
||||
{images.length > 0 ? <ListImages images={images} /> : <p className="text-muted-foreground italic">No images found.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
10
src/app/images/upload/page.tsx
Normal file
10
src/app/images/upload/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import UploadImageForm from "@/components/images/upload/UploadImageForm";
|
||||
|
||||
export default function ImagesUploadPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Upload image</h1>
|
||||
<UploadImageForm />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -23,7 +23,7 @@ export default function CreateGalleryForm() {
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof gallerySchema>) {
|
||||
var gallery = await createGallery(values)
|
||||
const gallery = await createGallery(values)
|
||||
if (gallery) {
|
||||
toast.success("Gallery created")
|
||||
router.push(`/galleries`)
|
||||
|
@ -27,6 +27,11 @@ export default function TopNav() {
|
||||
<Link href="/artists">Artists</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/images">Images</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
);
|
||||
|
152
src/components/images/edit/EditGalleryForm.tsx
Normal file
152
src/components/images/edit/EditGalleryForm.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
"use client"
|
||||
|
||||
import { deleteGallery } from "@/actions/galleries/deleteGallery";
|
||||
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 { gallerySchema } from "@/schemas/galleries/gallerySchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
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[] } }) {
|
||||
const router = useRouter();
|
||||
const form = useForm<z.infer<typeof gallerySchema>>({
|
||||
resolver: zodResolver(gallerySchema),
|
||||
defaultValues: {
|
||||
name: gallery.name,
|
||||
slug: gallery.slug,
|
||||
description: gallery.description || "",
|
||||
},
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof gallerySchema>) {
|
||||
var updatedGallery = await updateGallery(values, gallery.id)
|
||||
if (updatedGallery) {
|
||||
toast.success("Gallery updated")
|
||||
router.push(`/galleries`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gallery name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Gallery name" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gallery slug</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Gallery slug" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Will be used for the navigation.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gallery description</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Gallery description" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Description of the gallery.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="pt-10">
|
||||
<h2 className="text-lg font-semibold mb-2">Albums in this Gallery</h2>
|
||||
{gallery.albums.length === 0 ? (
|
||||
<p className="text-muted-foreground italic">No albums yet.</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{gallery.albums.map((album) => (
|
||||
<li key={album.id} className="flex items-center justify-between border rounded px-4 py-2">
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">{album.name}</div>
|
||||
<div className="text-sm text-muted-foreground">Slug: {album.slug}</div>
|
||||
</div>
|
||||
<div className="text-sm text-right">
|
||||
{/* Replace this with actual image count later */}
|
||||
<span className="font-mono">Images: 0</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Total images in this gallery: <span className="font-semibold">0</span>
|
||||
</p>
|
||||
<div>
|
||||
{gallery.albums.length === 0 ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={async () => {
|
||||
await deleteGallery(gallery.id);
|
||||
toast.success("Gallery deleted");
|
||||
router.push("/galleries");
|
||||
}}
|
||||
>
|
||||
Delete Gallery
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled
|
||||
onClick={async () => {
|
||||
await deleteGallery(gallery.id);
|
||||
toast.success("Gallery deleted");
|
||||
router.push("/galleries");
|
||||
}}
|
||||
>
|
||||
Delete Gallery
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
You must remove all albums before deleting this gallery.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
23
src/components/images/list/ListImages.tsx
Normal file
23
src/components/images/list/ListImages.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
// "use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Image } from "@/generated/prisma";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ListImages({ images }: { images: Image[] }) {
|
||||
return (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{images.map((image) => (
|
||||
<Link href={`/galleries/edit/${image.id}`} key={image.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{image.imageName}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
92
src/components/images/upload/UploadImageForm.tsx
Normal file
92
src/components/images/upload/UploadImageForm.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
"use client"
|
||||
|
||||
import { uploadImage } from "@/actions/images/uploadImage";
|
||||
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 { imageUploadSchema } from "@/schemas/images/imageSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
|
||||
export default function UploadImageForm() {
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<z.infer<typeof imageUploadSchema>>({
|
||||
resolver: zodResolver(imageUploadSchema),
|
||||
defaultValues: {
|
||||
file: undefined,
|
||||
imageName: "",
|
||||
},
|
||||
})
|
||||
|
||||
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
// setPreview(reader.result as string);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
form.setValue("file", files);
|
||||
};
|
||||
|
||||
async function onSubmit(values: z.infer<typeof imageUploadSchema>) {
|
||||
const image = await uploadImage(values)
|
||||
if (image) {
|
||||
toast.success("Image created")
|
||||
router.push(`/images/edit/${image.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="file"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Choose image to upload</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => onFileChange(e)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imageName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Image name" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
21
src/lib/s3.ts
Normal file
21
src/lib/s3.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
export const s3 = new S3Client({
|
||||
region: "us-east-1",
|
||||
endpoint: "http://10.0.20.11:9010",
|
||||
forcePathStyle: true,
|
||||
credentials: {
|
||||
accessKeyId: "fellies",
|
||||
secretAccessKey: "XCJ7spqxWZhVn8tkYnfVBFbz2cRKYxPAfeQeIdPRp1",
|
||||
},
|
||||
});
|
||||
|
||||
export async function getSignedImageUrl(key: string, expiresInSec = 3600) {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: "felliesartapp",
|
||||
Key: key,
|
||||
});
|
||||
|
||||
return getSignedUrl(s3, command, { expiresIn: expiresInSec });
|
||||
}
|
23
src/schemas/images/imageSchema.ts
Normal file
23
src/schemas/images/imageSchema.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const imageUploadSchema = z.object({
|
||||
imageName: z.string().min(1, "Image name is required"),
|
||||
file: z
|
||||
.custom<FileList>()
|
||||
.refine((files) => files instanceof FileList && files.length > 0, {
|
||||
message: "Image file is required",
|
||||
}),
|
||||
})
|
||||
|
||||
export const imageSchema = z.object({
|
||||
imageName: z.string().min(1, "Image name is required"),
|
||||
fileKey: z.string().min(1, "File key is required"),
|
||||
originalFile: z.string().min(1, "Original file is required"),
|
||||
uploadDate: z.date(),
|
||||
altText: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
imageData: z.string().optional(),
|
||||
creationMonth: z.coerce.number().int().min(1).max(12).optional(),
|
||||
creationYear: z.coerce.number().int().min(1900).max(2100).optional(),
|
||||
creationDate: z.date().optional(),
|
||||
})
|
4
src/types/VibrantSwatch.ts
Normal file
4
src/types/VibrantSwatch.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type VibrantSwatch = {
|
||||
_rgb: [number, number, number];
|
||||
hex?: string;
|
||||
};
|
4
src/types/tone.ts
Normal file
4
src/types/tone.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type Tone = {
|
||||
tone: number;
|
||||
hex: string
|
||||
};
|
74
src/utils/uploadHelper.ts
Normal file
74
src/utils/uploadHelper.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Tone } from "@/types/tone";
|
||||
import { TonalPalette } from "@material/material-color-utilities";
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function generatePaletteName(tones: Tone[]): string {
|
||||
const hexString = tones.map(t => t.hex.toLowerCase()).join('');
|
||||
const hash = crypto.createHash('sha256').update(hexString).digest('hex');
|
||||
return `palette-${hash.slice(0, 8)}`;
|
||||
}
|
||||
|
||||
export function rgbToHex(rgb: number[]): string {
|
||||
return `#${rgb
|
||||
.map((val) => Math.round(val).toString(16).padStart(2, "0"))
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
export function argbToHex(argb: number): string {
|
||||
return `#${argb.toString(16).slice(2).padStart(6, "0")}`;
|
||||
}
|
||||
|
||||
export function extractPaletteTones(
|
||||
palette: TonalPalette, tones: number[] = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
|
||||
) {
|
||||
return tones.map((t) => ({
|
||||
tone: t,
|
||||
hex: argbToHex(palette.tone(t)),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function upsertPalettes(tones: Tone[], imageId: string, type: string) {
|
||||
const paletteName = generatePaletteName(tones);
|
||||
|
||||
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;
|
||||
}
|
Reference in New Issue
Block a user