Add image upload function

This commit is contained in:
2025-06-26 21:21:52 +02:00
parent d608267a62
commit 0ccc01fb97
28 changed files with 4523 additions and 26 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View 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;

View File

@ -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;

View 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;

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Image" ADD COLUMN "fileSize" INTEGER,
ADD COLUMN "fileType" TEXT;

View 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;

View 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;

View File

@ -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";

View File

@ -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])
}

View 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

View File

@ -0,0 +1,7 @@
"use server";
import prisma from "@/lib/prisma";
export async function deleteImage(id: string) {
await prisma.gallery.delete({ where: { id } });
}

View 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,
}
})
}

View 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,
// }
// })
}

View 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
View 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>
);
}

View 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>
);
}

View File

@ -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`)

View File

@ -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>
);

View 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>
);
}

View 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>
);
}

View 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
View 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 });
}

View 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(),
})

View File

@ -0,0 +1,4 @@
export type VibrantSwatch = {
_rgb: [number, number, number];
hex?: string;
};

4
src/types/tone.ts Normal file
View File

@ -0,0 +1,4 @@
export type Tone = {
tone: number;
hex: string
};

74
src/utils/uploadHelper.ts Normal file
View 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;
}