25 Commits

Author SHA1 Message Date
e907de47a4 Add nsfw handling. Add zustand for global store 2026-02-04 01:12:00 +01:00
c4107718d0 Add tags to commssion types and custom types. Add button for example images to cards 2026-02-02 16:59:27 +01:00
1a855b2177 Fix moving of tags table 2026-02-02 15:18:26 +01:00
9121b74ade Hide debug 2026-02-02 11:45:51 +01:00
90c27ff60a Change gap calculation on gallery for better breakpoint on mobile and tablet 2026-02-02 10:51:37 +01:00
79b186889b Add debug for screen size 2026-02-02 10:40:12 +01:00
1952eb89a3 Fix dev Dockerfile 2026-02-02 00:52:23 +01:00
cee86edf44 Fix animal list display 2026-02-02 00:42:55 +01:00
26118d2897 Fix responsive layout on commission status for light mode 2026-02-02 00:15:37 +01:00
d70f00314b Fix responsive layout on artwork single page 2026-02-02 00:12:11 +01:00
874aa5f343 Fix responsive layout with max items per row for each breakpoint 2026-02-02 00:05:13 +01:00
b559b8250f Fix responsive layout with max items per row for each breakpoint 2026-02-01 23:28:09 +01:00
d6695e4c1d Fix incosistencies in the custom commission types 2026-02-01 23:21:35 +01:00
145770afbe Fix footer copyright year 2026-02-01 22:49:59 +01:00
0d1dd3b0fe Add versioning to footer 2026-02-01 22:37:53 +01:00
5f05557682 Update packages 2026-02-01 22:03:20 +01:00
ea354e5a9f Bump version to nextjs 16.1.6 2026-02-01 22:01:04 +01:00
940e934237 Add kanban board to page 2026-02-01 16:20:35 +01:00
aa95635e3e Add custom YCH typs for commission page 2026-02-01 16:08:08 +01:00
1940867519 Change overall look for dark mode and Timelapse View 2026-02-01 15:08:34 +01:00
3e6d045cbd Add commission type example image 2026-01-31 16:37:35 +01:00
c712f31759 Refactor code 2026-01-31 16:04:29 +01:00
eb8dcd54a8 Add rss feed with latest 10 artworks 2026-01-31 12:04:04 +01:00
030065631c Add rss feed with latest 10 artworks 2026-01-31 11:51:30 +01:00
5a3e567ed5 Add rss feed with latest 10 artworks 2026-01-31 11:46:21 +01:00
52 changed files with 1979 additions and 2349 deletions

View File

@ -18,6 +18,12 @@ RUN bunx prisma generate
# Uncomment the following line in case you want to disable telemetry during the build. # Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ARG GIT_SHA=unknown
ARG APP_VERSION=0.0.0
ARG DEPLOY_ENV=production
ENV NEXT_PUBLIC_GIT_SHA=$GIT_SHA \
NEXT_PUBLIC_APP_VERSION=$APP_VERSION \
NEXT_PUBLIC_DEPLOY_ENV=$DEPLOY_ENV
ARG DATABASE_URL ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL ENV DATABASE_URL=$DATABASE_URL
@ -33,6 +39,12 @@ ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production \ ENV NODE_ENV=production \
PORT=3000 \ PORT=3000 \
HOSTNAME="0.0.0.0" HOSTNAME="0.0.0.0"
ARG GIT_SHA=unknown
ARG APP_VERSION=0.0.0
ARG DEPLOY_ENV=production
ENV NEXT_PUBLIC_GIT_SHA=$GIT_SHA \
NEXT_PUBLIC_APP_VERSION=$APP_VERSION \
NEXT_PUBLIC_DEPLOY_ENV=$DEPLOY_ENV
RUN groupadd --system --gid 1001 nodejs && \ RUN groupadd --system --gid 1001 nodejs && \
useradd --system --uid 1001 --no-log-init -g nodejs nextjs useradd --system --uid 1001 --no-log-init -g nodejs nextjs

View File

@ -14,11 +14,16 @@ FROM base AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
# RUN bunx prisma migrate deploy
RUN bunx prisma generate RUN bunx prisma generate
# Uncomment the following line in case you want to disable telemetry during the build. # Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ARG GIT_SHA=unknown
ARG APP_VERSION=0.0.0
ARG DEPLOY_ENV=production
ENV NEXT_PUBLIC_GIT_SHA=$GIT_SHA \
NEXT_PUBLIC_APP_VERSION=$APP_VERSION \
NEXT_PUBLIC_DEPLOY_ENV=$DEPLOY_ENV
ARG DATABASE_URL ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL ENV DATABASE_URL=$DATABASE_URL
@ -34,6 +39,12 @@ ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production \ ENV NODE_ENV=production \
PORT=3000 \ PORT=3000 \
HOSTNAME="0.0.0.0" HOSTNAME="0.0.0.0"
ARG GIT_SHA=unknown
ARG APP_VERSION=0.0.0
ARG DEPLOY_ENV=production
ENV NEXT_PUBLIC_GIT_SHA=$GIT_SHA \
NEXT_PUBLIC_APP_VERSION=$APP_VERSION \
NEXT_PUBLIC_DEPLOY_ENV=$DEPLOY_ENV
RUN groupadd --system --gid 1001 nodejs && \ RUN groupadd --system --gid 1001 nodejs && \
useradd --system --uid 1001 --no-log-init -g nodejs nextjs useradd --system --uid 1001 --no-log-init -g nodejs nextjs

159
bun.lock
View File

@ -5,11 +5,11 @@
"": { "": {
"name": "app.gaertan.art", "name": "app.gaertan.art",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.962.0", "@aws-sdk/client-s3": "^3.980.0",
"@aws-sdk/s3-request-presigner": "^3.962.0", "@aws-sdk/s3-request-presigner": "^3.980.0",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@prisma/adapter-pg": "^7.2.0", "@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.2.0", "@prisma/client": "^7.3.0",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
@ -25,32 +25,37 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"lucide-react": "^0.561.0", "lucide-react": "^0.561.0",
"next": "^16.1.4", "next": "16.1.6",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pg": "^8.16.3", "pg": "^8.18.0",
"react": "19.2.1", "react": "19.2.4",
"react-dom": "19.2.1", "react-dom": "19.2.4",
"react-hook-form": "^7.69.0", "react-hook-form": "^7.71.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"simple-icons": "^16.3.0", "simple-icons": "^16.7.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"zod": "^4.3.4", "zod": "^4.3.6",
"zustand": "^5.0.6",
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.2.0", "@biomejs/biome": "2.2.0",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@types/node": "^20.19.27", "@types/node": "^20.19.30",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"@types/react": "^19.2.7", "@types/react": "19.2.10",
"@types/react-dom": "^19.2.3", "@types/react-dom": "19.2.3",
"prisma": "^7.2.0", "prisma": "^7.3.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
}, },
}, },
}, },
"overrides": {
"@types/react": "19.2.10",
"@types/react-dom": "19.2.3",
},
"packages": { "packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@ -68,75 +73,75 @@
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.974.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/credential-provider-node": "^3.972.1", "@aws-sdk/middleware-bucket-endpoint": "^3.972.1", "@aws-sdk/middleware-expect-continue": "^3.972.1", "@aws-sdk/middleware-flexible-checksums": "^3.972.1", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-location-constraint": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-sdk-s3": "^3.972.1", "@aws-sdk/middleware-ssec": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/signature-v4-multi-region": "3.972.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-blob-browser": "^4.2.9", "@smithy/hash-node": "^4.2.8", "@smithy/hash-stream-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/md5-js": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-X+vpXNJ8cU8Iw1FtDgDHxo9z6RxlXfcTtpdGnKws4rk+tCYKSAor/DG6BRMzbh4E5xAA7DiU1Ny3BTrRRSt/Yg=="], "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.980.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.5", "@aws-sdk/credential-provider-node": "^3.972.4", "@aws-sdk/middleware-bucket-endpoint": "^3.972.3", "@aws-sdk/middleware-expect-continue": "^3.972.3", "@aws-sdk/middleware-flexible-checksums": "^3.972.3", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-location-constraint": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-sdk-s3": "^3.972.5", "@aws-sdk/middleware-ssec": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.5", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/signature-v4-multi-region": "3.980.0", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.980.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.3", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.22.0", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-blob-browser": "^4.2.9", "@smithy/hash-node": "^4.2.8", "@smithy/hash-stream-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/md5-js": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/middleware-retry": "^4.4.29", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.28", "@smithy/util-defaults-mode-node": "^4.2.31", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-ch8QqKehyn1WOYbd8LyDbWjv84Z9OEj9qUxz8q3IOCU3ftAVkVR0wAuN96a1xCHnpOJcQZo3rOB08RlyKdkGxQ=="],
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ci+GiM0c4ULo4D79UMcY06LcOLcfvUfiyt8PzNY0vbt5O8BfCPYf4QomwVgkNcLLCYmroO4ge2Yy1EsLUlcD6g=="], "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.980.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.5", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.5", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.980.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.3", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.22.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/middleware-retry": "^4.4.29", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.28", "@smithy/util-defaults-mode-node": "^4.2.31", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-AhNXQaJ46C1I+lQ+6Kj+L24il5K9lqqIanJd8lMszPmP7bLnmX0wTKK0dxywcvrLdij3zhWttjAKEBNgLtS8/A=="],
"@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="], "@aws-sdk/core": ["@aws-sdk/core@3.973.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.2", "@smithy/core": "^3.22.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-IMM7xGfLGW6lMvubsA4j6BHU5FPgGAxoQ/NA63KqNLMwTS+PeMBcx8DPHL12Vg6yqOZnqok9Mu4H2BdQyq7gSA=="],
"@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw=="], "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw=="],
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-/etNHqnx96phy/SjI0HRC588o4vKH5F0xfkZ13yAATV7aNrb+5gYGNE6ePWafP+FuZ3HkULSSlJFj0AxgrAqYw=="], "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.3", "", { "dependencies": { "@aws-sdk/core": "^3.973.5", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-OBYNY4xQPq7Rx+oOhtyuyO0AQvdJSpXRg7JuPNBJH4a1XXIzJQl4UHQTPKZKwfJXmYLpv4+OkcFen4LYmDPd3g=="],
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" } }, "sha512-AeopObGW5lpWbDRZ+t4EAtS7wdfSrHPLeFts7jaBzgIaCCD7TL7jAyAB9Y5bCLOPF+17+GL54djCCsjePljUAw=="], "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.5", "", { "dependencies": { "@aws-sdk/core": "^3.973.5", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" } }, "sha512-GpvBgEmSZPvlDekd26Zi+XsI27Qz7y0utUx0g2fSTSiDzhnd1FSa1owuodxR0BcUKNL7U2cOVhhDxgZ4iSoPVg=="],
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/credential-provider-env": "^3.972.1", "@aws-sdk/credential-provider-http": "^3.972.1", "@aws-sdk/credential-provider-login": "^3.972.1", "@aws-sdk/credential-provider-process": "^3.972.1", "@aws-sdk/credential-provider-sso": "^3.972.1", "@aws-sdk/credential-provider-web-identity": "^3.972.1", "@aws-sdk/nested-clients": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-OdbJA3v+XlNDsrYzNPRUwr8l7gw1r/nR8l4r96MDzSBDU8WEo8T6C06SvwaXR8SpzsjO3sq5KMP86wXWg7Rj4g=="], "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.3", "", { "dependencies": { "@aws-sdk/core": "^3.973.5", "@aws-sdk/credential-provider-env": "^3.972.3", "@aws-sdk/credential-provider-http": "^3.972.5", "@aws-sdk/credential-provider-login": "^3.972.3", "@aws-sdk/credential-provider-process": "^3.972.3", "@aws-sdk/credential-provider-sso": "^3.972.3", "@aws-sdk/credential-provider-web-identity": "^3.972.3", "@aws-sdk/nested-clients": "3.980.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-rMQAIxstP7cLgYfsRGrGOlpyMl0l8JL2mcke3dsIPLWke05zKOFyR7yoJzWCsI/QiIxjRbxpvPiAeKEA6CoYkg=="],
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/nested-clients": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CccqDGL6ZrF3/EFWZefvKW7QwwRdxlHUO8NVBKNVcNq6womrPDvqB6xc9icACtE0XB0a7PLoSTkAg8bQVkTO2w=="], "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.3", "", { "dependencies": { "@aws-sdk/core": "^3.973.5", "@aws-sdk/nested-clients": "3.980.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Gc3O91iVvA47kp2CLIXOwuo5ffo1cIpmmyIewcYjAcvurdFHQ8YdcBe1KHidnbbBO4/ZtywGBACsAX5vr3UdoA=="],
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.1", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.1", "@aws-sdk/credential-provider-http": "^3.972.1", "@aws-sdk/credential-provider-ini": "^3.972.1", "@aws-sdk/credential-provider-process": "^3.972.1", "@aws-sdk/credential-provider-sso": "^3.972.1", "@aws-sdk/credential-provider-web-identity": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwXPk9GfuU/xG9tmCyXFVkCr6X3W8ZCoL5Ptb0pbltEx1/LCcg7T+PBqDlPiiinNCD6ilIoMJDWsnJ8ikzZA7Q=="], "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.4", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.3", "@aws-sdk/credential-provider-http": "^3.972.5", "@aws-sdk/credential-provider-ini": "^3.972.3", "@aws-sdk/credential-provider-process": "^3.972.3", "@aws-sdk/credential-provider-sso": "^3.972.3", "@aws-sdk/credential-provider-web-identity": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-UwerdzosMSY7V5oIZm3NsMDZPv2aSVzSkZxYxIOWHBeKTZlUqW7XpHtJMZ4PZpJ+HMRhgP+MDGQx4THndgqJfQ=="],
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-bi47Zigu3692SJwdBvo8y1dEwE6B61stCwCFnuRWJVTfiM84B+VTSCV661CSWJmIZzmcy7J5J3kWyxL02iHj0w=="], "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.3", "", { "dependencies": { "@aws-sdk/core": "^3.973.5", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-xkSY7zjRqeVc6TXK2xr3z1bTLm0wD8cj3lAkproRGaO4Ku7dPlKy843YKnHrUOUzOnMezdZ4xtmFc0eKIDTo2w=="],
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.1", "", { "dependencies": { "@aws-sdk/client-sso": "3.974.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/token-providers": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-dLZVNhM7wSgVUFsgVYgI5hb5Z/9PUkT46pk/SHrSmUqfx6YDvoV4YcPtaiRqviPpEGGiRtdQMEadyOKIRqulUQ=="], "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.3", "", { "dependencies": { "@aws-sdk/client-sso": "3.980.0", "@aws-sdk/core": "^3.973.5", "@aws-sdk/token-providers": "3.980.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8Ww3F5Ngk8dZ6JPL/V5LhCU1BwMfQd3tLdoEuzaewX8FdnT633tPr+KTHySz9FK7fFPcz5qG3R5edVEhWQD4AA=="],
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/nested-clients": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-YMDeYgi0u687Ay0dAq/pFPKuijrlKTgsaB/UATbxCs/FzZfMiG4If5ksywHmmW7MiYUF8VVv+uou3TczvLrN4w=="], "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.3", "", { "dependencies": { "@aws-sdk/core": "^3.973.5", "@aws-sdk/nested-clients": "3.980.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-62VufdcH5rRfiRKZRcf1wVbbt/1jAntMj1+J0qAd+r5pQRg2t0/P9/Rz16B1o5/0Se9lVL506LRjrhIJAhYBfA=="],
"@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-arn-parser": "^3.972.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-YVvoitBdE8WOpHqIXvv49efT73F4bJ99XH2bi3Dn3mx7WngI4RwHwn/zF5i0q1Wdi5frGSCNF3vuh+pY817//w=="], "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-arn-parser": "^3.972.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg=="],
"@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6lfl2/J/kutzw/RLu1kjbahsz4vrGPysrdxWaw8fkjLYG+6M6AswocIAZFS/LgAVi/IWRwPTx9YC0/NH2wDrSw=="], "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg=="],
"@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.972.1", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/crc64-nvme": "3.972.0", "@aws-sdk/types": "^3.973.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kjVVREpqeUkYQsXr78AcsJbEUlxGH7+H6yS7zkjrnu6HyEVxbdSndkKX6VpKneFOihjCAhIXlk4wf3butDHkNQ=="], "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.972.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.5", "@aws-sdk/crc64-nvme": "3.972.0", "@aws-sdk/types": "^3.973.1", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-MkNGJ6qB9kpsLwL18kC/ZXppsJbftHVGCisqpEVbTQsum8CLYDX1Bmp/IvhRGNxsqCO2w9/4PwhDKBjG3Uvr4Q=="],
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-/R82lXLPmZ9JaUGSUdKtBp2k/5xQxvBT3zZWyKiBOhyulFotlfvdlrO8TnqstBimsl4lYEYySDL+W6ldFh6ALg=="], "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA=="],
"@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-YisPaCbvBk9gY5aUI8jDMDKXsLZ9Fet0WYj1MviK8tZYMgxBIYHM6l3O/OHaAIujojZvamd9F3haYYYWp5/V3w=="], "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g=="],
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-JGgFl6cHg9G2FHu4lyFIzmFN8KESBiRr84gLC3Aeni0Gt1nKm+KxWLBuha/RPcXxJygGXCcMM4AykkIwxor8RA=="], "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA=="],
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-taGzNRe8vPHjnliqXIHp9kBgIemLE/xCaRTMH1NH0cncHeaPcjxtnCroAAM9aOlPuKvBe2CpZESyvM1+D8oI7Q=="], "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q=="],
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-arn-parser": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-q/hK0ZNf/aafFRv2wIlDM3p+izi5cXwktVNvRvW646A0MvVZmT4/vwadv/jPA9AORFbnpyf/0luxiMz181f9yg=="], "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.5", "", { "dependencies": { "@aws-sdk/core": "^3.973.5", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-arn-parser": "^3.972.2", "@smithy/core": "^3.22.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-3IgeIDiQ15tmMBFIdJ1cTy3A9rXHGo+b9p22V38vA3MozeMyVC8VmCYdDLA0iMWo4VHA9LDJTgCM0+xU3wjBOg=="],
"@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-fLtRTPd/MxJT2drJKft2GVGKm35PiNEeQ1Dvz1vc/WhhgAteYrp4f1SfSgjgLaYWGMExESJL4bt8Dxqp6tVsog=="], "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg=="],
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@smithy/core": "^3.21.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6SVg4pY/9Oq9MLzO48xuM3lsOb8Rxg55qprEtFRpkUmuvKij31f5SQHEGxuiZ4RqIKrfjr2WMuIgXvqJ0eJsPA=="], "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.5", "", { "dependencies": { "@aws-sdk/core": "^3.973.5", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.980.0", "@smithy/core": "^3.22.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-TVZQ6PWPwQbahUI8V+Er+gS41ctIawcI/uMNmQtQ7RMcg3JYn6gyKAFKUb3HFYx2OjYlx1u11sETSwwEUxVHTg=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q=="], "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.980.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.5", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.5", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.980.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.3", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.22.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/middleware-retry": "^4.4.29", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.28", "@smithy/util-defaults-mode-node": "^4.2.31", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ=="],
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-voIY8RORpxLAEgEkYaTFnkaIuRwVBEc+RjVZYcSSllPV+ZEKAacai6kNhJeE3D70Le+JCfvRb52tng/AVHY+jQ=="], "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow=="],
"@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.974.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.972.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-format-url": "^3.972.1", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-tApmJb4XXBdNQzxTYIBq9aYj8vjJqiMPyeUF25wzvGjLQfXgvcv5sTR4yyzXBxRc8+O7quWDBgMJGtcNerapRQ=="], "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.980.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.980.0", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-format-url": "^3.972.3", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-qX1Ptvja9Le0Wt1VadgsJ7Kw8Xf57pTIVmIcvYD5HrdAot71qgXdfBtcbuvNKZPeD+HfcUITwxxHpDiXfSoTsA=="],
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.972.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.972.0", "@aws-sdk/types": "3.972.0", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-2udiRijmjpN81Pvajje4TsjbXDZNP6K9bYUanBYH8hXa/tZG5qfGCySD+TyX0sgDxCQmEDMg3LaQdfjNHBDEgQ=="], "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.980.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.5", "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-tO2jBj+ZIVM0nEgi1SyxWtaYGpuAJdsrugmWcI3/U2MPWCYsrvKasUo0026NvJJao38wyUq9B8XTG8Xu53j/VA=="],
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.974.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/nested-clients": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cBykL0LiccKIgNhGWvQRTPvsBLPZxnmJU3pYxG538jpFX8lQtrCy1L7mmIHNEdxIdIGEPgAEHF8/JQxgBToqUQ=="], "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.980.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.5", "@aws-sdk/nested-clients": "3.980.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1nFileg1wAgDmieRoj9dOawgr2hhlh7xdvcH57b1NnqfPaVlcqVJyPc6k3TLDUFPY69eEwNxdGue/0wIz58vjA=="],
"@aws-sdk/types": ["@aws-sdk/types@3.973.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-jYIdB7a7jhRTvyb378nsjyvJh1Si+zVduJ6urMNGpz8RjkmHZ+9vM2H07XaIB2Cfq0GhJRZYOfUCH8uqQhqBkQ=="], "@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="],
"@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-XnNit6H9PPHhqUXW/usjX6JeJ6Pm8ZNqivTjmNjgWHeOfVpblUc/MTic02UmCNR0jJLPjQ3mBKiMen0tnkNQjQ=="], "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg=="],
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.972.0", "", { "dependencies": { "@aws-sdk/types": "3.972.0", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg=="], "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.980.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw=="],
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8wJ4/XOLU/RIYBHsXsIOTR04bNmalC8F2YPMyf3oL8YC750M3Rv5WGywW0Fo07HCv770KXJOzVq03Gyl68moFg=="], "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g=="],
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.953.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mPxK+I1LcrgC/RSa3G5AMAn8eN2Ay0VOgw8lSRmV1jCtO+iYvNeCqOdxoJUjOW6I5BA4niIRWqVORuRP07776Q=="], "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.953.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mPxK+I1LcrgC/RSa3G5AMAn8eN2Ay0VOgw8lSRmV1jCtO+iYvNeCqOdxoJUjOW6I5BA4niIRWqVORuRP07776Q=="],
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-IgF55NFmJX8d9Wql9M0nEpk2eYbuD8G4781FN4/fFgwTXBn86DvlZJuRWDCMcMqZymnBVX7HW9r+3r9ylqfW0w=="], "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw=="],
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.1", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-oIs4JFcADzoZ0c915R83XvK2HltWupxNsXUIuZse2rgk7b97zTpkxaqXiH0h9ylh31qtgo/t8hp4tIqcsMrEbQ=="], "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.3", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.5", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-gqG+02/lXQtO0j3US6EVnxtwwoXQC5l2qkhLCrqUrqdtcQxV7FDMbm9wLjKqoronSHyELGTjbFKK/xV5q1bZNA=="],
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-6zZGlPOqn7Xb+25MAXGb1JhgvaC5HjZj6GzszuVrnEgbhvzBRFGKYemuHBV4bho+dtqeYKPgaZUv7/e80hIGNg=="], "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.2", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-jGOOV/bV1DhkkUhHiZ3/1GZ67cZyOXaDb7d1rYD6ZiXf5V9tBNOcgqXwRRPvrCbYaFRa1pPMFb3ZjqjWpR3YfA=="],
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.2", "", {}, "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.2", "", {}, "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg=="],
@ -248,23 +253,23 @@
"@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="],
"@next/env": ["@next/env@16.1.4", "", {}, "sha512-gkrXnZyxPUy0Gg6SrPQPccbNVLSP3vmW8LU5dwEttEEC1RwDivk8w4O+sZIjFvPrSICXyhQDCG+y3VmjlJf+9A=="], "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-T8atLKuvk13XQUdVLCv1ZzMPgLPW0+DWWbHSQXs0/3TjPrKNxTmUIhOEaoEyl3Z82k8h/gEtqyuoZGv6+Ugawg=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-AKC/qVjUGUQDSPI6gESTx0xOnOPQ5gttogNS3o6bA83yiaSZJek0Am5yXy82F1KcZCx3DdOwdGPZpQCluonuxg=="], "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-POQ65+pnYOkZNdngWfMEt7r53bzWiKkVNbjpmCt1Zb3V6lxJNXSsjwRuTQ8P/kguxDC8LRkqaL3vvsFrce4dMQ=="], "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q=="], "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ=="], "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw=="], "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw=="], "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-JSVlm9MDhmTXw/sO2PE/MRj+G6XOSMZB+BcZ0a7d6KwVFZVpkHcb2okyoYFBaco6LeiL53BBklRlOrDDbOeE5w=="], "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="],
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.3.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.3.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg=="], "@prisma/adapter-pg": ["@prisma/adapter-pg@7.3.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.3.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg=="],
@ -376,7 +381,7 @@
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="], "@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="],
"@smithy/core": ["@smithy/core@3.21.1", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-NUH8R4O6FkN8HKMojzbGg/5pNjsfTjlMmeFclyPfPaXXUrbr5TzhWgbf7t92wfrpCHRgpjyz7ffASIS3wX28aA=="], "@smithy/core": ["@smithy/core@3.22.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-6vjCHD6vaY8KubeNw2Fg3EK0KLGQYdldG4fYgQmA0xSW0dJ8G2xFhSOdrlUakWVoP5JuWHtFODg3PNd/DN3FDA=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="],
@ -406,9 +411,9 @@
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="], "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="],
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.11", "", { "dependencies": { "@smithy/core": "^3.21.1", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-/WqsrycweGGfb9sSzME4CrsuayjJF6BueBmkKlcbeU5q18OhxRrvvKlmfw3tpDsK5ilx2XUJvoukwxHB0nHs/Q=="], "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.12", "", { "dependencies": { "@smithy/core": "^3.22.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-9JMKHVJtW9RysTNjcBZQHDwB0p3iTP6B1IfQV4m+uCevkVd/VuLgwfqk5cnI4RHcp4cPwoIvxQqN4B1sxeHo8Q=="],
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.27", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-xFUYCGRVsfgiN5EjsJJSzih9+yjStgMTCLANPlf0LVQkPDYCe0hz97qbdTZosFOiYlGBlHYityGRxrQ/hxhfVQ=="], "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.29", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-bmTn75a4tmKRkC5w61yYQLb3DmxNzB8qSVu9SbTYqW6GAL0WXO2bDZuMAn/GJSbOdHEdjZvWxe+9Kk015bw6Cg=="],
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="], "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="],
@ -432,7 +437,7 @@
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="], "@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="],
"@smithy/smithy-client": ["@smithy/smithy-client@4.10.12", "", { "dependencies": { "@smithy/core": "^3.21.1", "@smithy/middleware-endpoint": "^4.4.11", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" } }, "sha512-VKO/HKoQ5OrSHW6AJUmEnUKeXI1/5LfCwO9cwyao7CmLvGnZeM1i36Lyful3LK1XU7HwTVieTqO1y2C/6t3qtA=="], "@smithy/smithy-client": ["@smithy/smithy-client@4.11.1", "", { "dependencies": { "@smithy/core": "^3.22.0", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" } }, "sha512-SERgNg5Z1U+jfR6/2xPYjSEHY1t3pyTHC/Ma3YQl6qWtmiL42bvNId3W/oMUWIwu7ekL2FMPdqAmwbQegM7HeQ=="],
"@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="], "@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="],
@ -448,9 +453,9 @@
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.26", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-vva0dzYUTgn7DdE0uaha10uEdAgmdLnNFowKFjpMm6p2R0XDk5FHPX3CBJLzWQkQXuEprsb0hGz9YwbicNWhjw=="], "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.28", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-/9zcatsCao9h6g18p/9vH9NIi5PSqhCkxQ/tb7pMgRFnqYp9XUOyOlGPDMHzr8n5ih6yYgwJEY2MLEobUgi47w=="],
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.29", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-c6D7IUBsZt/aNnTBHMTf+OVh+h/JcxUUgfTcIJaWRe6zhOum1X+pNKSZtZ+7fbOn5I99XVFtmrnXKv8yHHErTQ=="], "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.31", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-JTvoApUXA5kbpceI2vuqQzRjeTbLpx1eoa5R/YEZbTgtxvIB7AQZxFJ0SEyfCpgPCyVV9IT7we+ytSeIB3CyWA=="],
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="], "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="],
@ -522,7 +527,7 @@
"@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="],
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="], "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
@ -764,7 +769,7 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"next": ["next@16.1.4", "", { "dependencies": { "@next/env": "16.1.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.4", "@next/swc-darwin-x64": "16.1.4", "@next/swc-linux-arm64-gnu": "16.1.4", "@next/swc-linux-arm64-musl": "16.1.4", "@next/swc-linux-x64-gnu": "16.1.4", "@next/swc-linux-x64-musl": "16.1.4", "@next/swc-win32-arm64-msvc": "16.1.4", "@next/swc-win32-x64-msvc": "16.1.4", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ=="], "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
@ -782,11 +787,11 @@
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="], "pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="],
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
"pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="], "pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
@ -824,9 +829,9 @@
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
"react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="], "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-hook-form": ["react-hook-form@7.71.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w=="], "react-hook-form": ["react-hook-form@7.71.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w=="],
@ -866,7 +871,7 @@
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-icons": ["simple-icons@16.6.0", "", {}, "sha512-lzSVlAhflhwud7EprwSalbCpHKpculOfaAk1P+S3QajO1bHG5nqwI1VeGnn4rwaE4xSSSKDsOFFL0XfIDv5iIQ=="], "simple-icons": ["simple-icons@16.7.0", "", {}, "sha512-2BteuQXu1+/jK5dF8YXe8nwoRG3afwl03bCmgPJLiotllUBU46B+WntXT731Z17oOntfV5eeTI7Q1k7MeOA6eg=="],
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
@ -940,6 +945,8 @@
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.953.0", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g=="], "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.953.0", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g=="],
@ -960,11 +967,7 @@
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.0", "", { "dependencies": { "@aws-sdk/core": "3.972.0", "@aws-sdk/types": "3.972.0", "@aws-sdk/util-arn-parser": "3.972.0", "@smithy/core": "^3.20.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-0bcKFXWx+NZ7tIlOo7KjQ+O2rydiHdIQahrq+fN6k9Osky29v17guy68urUKfhTobR6iY6KvxkroFWaFtTgS5w=="], "@prisma/adapter-pg/pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="],
"@aws-sdk/signature-v4-multi-region/@aws-sdk/types": ["@aws-sdk/types@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug=="],
"@aws-sdk/util-endpoints/@aws-sdk/types": ["@aws-sdk/types@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug=="],
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="], "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="],
@ -1016,16 +1019,14 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core": ["@aws-sdk/core@3.972.0", "", { "dependencies": { "@aws-sdk/types": "3.972.0", "@aws-sdk/xml-builder": "3.972.0", "@smithy/core": "^3.20.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A=="], "@prisma/adapter-pg/pg/pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="],
"@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-RM5Mmo/KJ593iMSrALlHEOcc9YOIyOsDmS5x2NLOMdEmzv1o00fcpAkCQ02IGu1eFneBFT7uX0Mpag0HI+Cz2g=="], "@prisma/adapter-pg/pg/pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg=="],
} }
} }

1815
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,19 @@
{ {
"name": "app.gaertan.art", "name": "app.gaertan.art",
"version": "0.1.0", "version": "1.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"dev:version": "NEXT_PUBLIC_GIT_SHA=$(git rev-parse --short HEAD) NEXT_PUBLIC_DEPLOY_ENV=dev bun dev",
"build": "next build", "build": "next build",
"build:version": "NEXT_PUBLIC_GIT_SHA=$(git rev-parse --short HEAD) NEXT_PUBLIC_DEPLOY_ENV=prod bun run build",
"start": "next start", "start": "next start",
"lint": "biome check", "lint": "biome check",
"format": "biome format --write" "format": "biome format --write"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.974.0", "@aws-sdk/client-s3": "^3.980.0",
"@aws-sdk/s3-request-presigner": "^3.974.0", "@aws-sdk/s3-request-presigner": "^3.980.0",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@prisma/adapter-pg": "^7.3.0", "@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.3.0", "@prisma/client": "^7.3.0",
@ -30,16 +32,17 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"lucide-react": "^0.561.0", "lucide-react": "^0.561.0",
"next": "^16.1.4", "next": "16.1.6",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pg": "^8.17.2", "pg": "^8.18.0",
"react": "19.2.1", "react": "19.2.4",
"react-dom": "19.2.1", "react-dom": "19.2.4",
"react-hook-form": "^7.71.1", "react-hook-form": "^7.71.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"simple-icons": "^16.6.0", "simple-icons": "^16.7.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"zustand": "^5.0.6",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
@ -47,11 +50,15 @@
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@types/node": "^20.19.30", "@types/node": "^20.19.30",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"@types/react": "^19.2.9", "@types/react": "19.2.10",
"@types/react-dom": "^19.2.3", "@types/react-dom": "19.2.3",
"prisma": "^7.3.0", "prisma": "^7.3.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
},
"overrides": {
"@types/react": "19.2.10",
"@types/react-dom": "19.2.3"
} }
} }

View File

@ -47,13 +47,13 @@ model Artwork {
galleryId String? galleryId String?
gallery Gallery? @relation(fields: [galleryId], references: [id]) gallery Gallery? @relation(fields: [galleryId], references: [id])
metadata ArtworkMetadata? metadata ArtworkMetadata?
timelapse ArtworkTimelapse? timelapse ArtworkTimelapse?
albums Album[] albums Album[]
categories ArtCategory[] categories ArtCategory[]
colors ArtworkColor[] colors ArtworkColor[]
tags ArtTag[] tags Tag[] @relation("ArtworkTags")
variants FileVariant[] variants FileVariant[]
@@index([colorStatus]) @@index([colorStatus])
@ -101,43 +101,7 @@ model ArtCategory {
description String? description String?
artworks Artwork[] artworks Artwork[]
tags ArtTag[] tagLinks TagCategory[]
}
model ArtTag {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sortIndex Int @default(0)
name String @unique
slug String @unique
isParent Boolean @default(false)
showOnAnimalPage Boolean @default(false)
description String?
aliases ArtTagAlias[]
artworks Artwork[]
categories ArtCategory[]
parentId String?
parent ArtTag? @relation("TagHierarchy", fields: [parentId], references: [id], onDelete: SetNull)
children ArtTag[] @relation("TagHierarchy")
}
model ArtTagAlias {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
alias String @unique
tagId String
tag ArtTag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@unique([tagId, alias])
@@index([alias])
} }
model Color { model Color {
@ -201,12 +165,12 @@ model ArtworkTimelapse {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
artworkId String @unique artworkId String @unique
artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade) artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade)
enabled Boolean @default(false) enabled Boolean @default(false)
s3Key String @unique s3Key String @unique
fileName String? fileName String?
mimeType String? mimeType String?
sizeBytes Int? sizeBytes Int?
@ -248,6 +212,72 @@ model FileVariant {
@@unique([artworkId, type]) @@unique([artworkId, type])
} }
model Tag {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sortIndex Int @default(0)
name String @unique
slug String @unique
isVisible Boolean @default(true)
description String?
aliases TagAlias[]
categoryLinks TagCategory[]
categoryParents TagCategory[] @relation("TagCategoryParent")
artworks Artwork[] @relation("ArtworkTags")
commissionTypes CommissionType[] @relation("CommissionTypeTags")
commissionCustomCards CommissionCustomCard[] @relation("CommissionCustomCardTags")
miniatures Miniature[] @relation("MiniatureTags")
}
model TagAlias {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
alias String @unique
tagId String
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@unique([tagId, alias])
@@index([alias])
}
model TagCategory {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tagId String
categoryId String
isParent Boolean @default(false)
showOnAnimalPage Boolean @default(false)
parentTagId String?
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
category ArtCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade)
parentTag Tag? @relation("TagCategoryParent", fields: [parentTagId], references: [id], onDelete: SetNull)
@@unique([tagId, categoryId])
@@index([categoryId])
@@index([tagId])
@@index([parentTagId])
@@index([categoryId, parentTagId])
}
model Miniature {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tags Tag[] @relation("MiniatureTags")
}
model Commission { model Commission {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -265,12 +295,35 @@ model CommissionType {
description String? description String?
tags Tag[] @relation("CommissionTypeTags")
options CommissionTypeOption[] options CommissionTypeOption[]
extras CommissionTypeExtra[] extras CommissionTypeExtra[]
customInputs CommissionTypeCustomInput[] customInputs CommissionTypeCustomInput[]
requests CommissionRequest[] requests CommissionRequest[]
} }
model CommissionCustomCard {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sortIndex Int @default(0)
name String
description String?
referenceImageUrl String?
isVisible Boolean @default(true)
isSpecialOffer Boolean @default(false)
tags Tag[] @relation("CommissionCustomCardTags")
options CommissionCustomCardOption[]
extras CommissionCustomCardExtra[]
requests CommissionRequest[]
@@index([isVisible, sortIndex])
}
model CommissionOption { model CommissionOption {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -281,8 +334,9 @@ model CommissionOption {
description String? description String?
types CommissionTypeOption[] types CommissionTypeOption[]
requests CommissionRequest[] customCards CommissionCustomCardOption[]
requests CommissionRequest[]
} }
model CommissionTypeOption { model CommissionTypeOption {
@ -314,8 +368,9 @@ model CommissionExtra {
description String? description String?
requests CommissionRequest[] requests CommissionRequest[]
types CommissionTypeExtra[] types CommissionTypeExtra[]
customCards CommissionCustomCardExtra[]
} }
model CommissionTypeExtra { model CommissionTypeExtra {
@ -337,6 +392,25 @@ model CommissionTypeExtra {
@@unique([typeId, extraId]) @@unique([typeId, extraId])
} }
model CommissionCustomCardOption {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sortIndex Int @default(0)
cardId String
optionId String
priceRange String?
pricePercent Float?
price Float?
card CommissionCustomCard @relation(fields: [cardId], references: [id], onDelete: Cascade)
option CommissionOption @relation(fields: [optionId], references: [id])
@@unique([cardId, optionId])
}
model CommissionCustomInput { model CommissionCustomInput {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -368,6 +442,25 @@ model CommissionTypeCustomInput {
@@unique([typeId, customInputId]) @@unique([typeId, customInputId])
} }
model CommissionCustomCardExtra {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sortIndex Int @default(0)
cardId String
extraId String
priceRange String?
pricePercent Float?
price Float?
card CommissionCustomCard @relation(fields: [cardId], references: [id], onDelete: Cascade)
extra CommissionExtra @relation(fields: [extraId], references: [id])
@@unique([cardId, extraId])
}
model CommissionRequest { model CommissionRequest {
id String @id @default(cuid()) id String @id @default(cuid())
index Int @default(autoincrement()) index Int @default(autoincrement())
@ -384,10 +477,12 @@ model CommissionRequest {
userAgent String? userAgent String?
customFields Json? customFields Json?
optionId String? optionId String?
typeId String? typeId String?
option CommissionOption? @relation(fields: [optionId], references: [id]) customCardId String?
type CommissionType? @relation(fields: [typeId], references: [id]) option CommissionOption? @relation(fields: [optionId], references: [id])
type CommissionType? @relation(fields: [typeId], references: [id])
customCard CommissionCustomCard? @relation(fields: [customCardId], references: [id])
extras CommissionExtra[] extras CommissionExtra[]
files CommissionRequestFile[] files CommissionRequestFile[]
@ -398,8 +493,9 @@ model CommissionGuidelines {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
markdown String markdown String
isActive Boolean @default(true) exampleImageUrl String?
isActive Boolean @default(true)
@@index([isActive]) @@index([isActive])
} }

View File

@ -51,6 +51,7 @@ export async function getAnimalStudiesPage(input: unknown): Promise<AnimalStudie
id: true, id: true,
name: true, name: true,
altText: true, altText: true,
nsfw: true,
sortKey: true, sortKey: true,
file: { select: { fileKey: true } }, file: { select: { fileKey: true } },
variants: { variants: {
@ -80,6 +81,7 @@ export async function getAnimalStudiesPage(input: unknown): Promise<AnimalStudie
id: r.id, id: r.id,
name: r.name, name: r.name,
altText: r.altText, altText: r.altText,
nsfw: r.nsfw ?? false,
fileKey: r.file.fileKey, fileKey: r.file.fileKey,
width: w, width: w,
height: h, height: h,

View File

@ -2,17 +2,9 @@
import { z } from "zod"; import { z } from "zod";
/**
* Server action
* Forwards a multipart/form-data request (payload + files[])
* from the public app to the admin app's public commissions endpoint.
*
* Server-only env required:
* ADMIN_URL=https://admin.domain.com
*/
const submitPayloadSchema = z.object({ const submitPayloadSchema = z.object({
typeId: z.string().optional().nullable(), typeId: z.string().optional().nullable(),
customCardId: z.string().optional().nullable(),
optionId: z.string().optional().nullable(), optionId: z.string().optional().nullable(),
extraIds: z.array(z.string()).default([]), extraIds: z.array(z.string()).default([]),
@ -20,6 +12,23 @@ const submitPayloadSchema = z.object({
customerEmail: z.string().email(), customerEmail: z.string().email(),
customerSocials: z.string().optional().nullable(), customerSocials: z.string().optional().nullable(),
message: z.string().min(1), message: z.string().min(1),
}).superRefine((data, ctx) => {
const hasType = Boolean(data.typeId);
const hasCustom = Boolean(data.customCardId);
if (!hasType && !hasCustom) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["typeId"],
message: "Missing commission type or custom card",
});
}
if (hasType && hasCustom) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["typeId"],
message: "Only one of typeId or customCardId is allowed",
});
}
}); });
export type SubmitCommissionPayload = z.infer<typeof submitPayloadSchema>; export type SubmitCommissionPayload = z.infer<typeof submitPayloadSchema>;
@ -36,7 +45,6 @@ export async function submitCommissionRequest(input: {
const payload = submitPayloadSchema.parse(input.payload); const payload = submitPayloadSchema.parse(input.payload);
const files = input.files ?? []; const files = input.files ?? [];
// Optional safety limits
const MAX_FILES = 10; const MAX_FILES = 10;
const MAX_BYTES_EACH = 10 * 1024 * 1024; // 10MB const MAX_BYTES_EACH = 10 * 1024 * 1024; // 10MB
@ -70,7 +78,6 @@ export async function submitCommissionRequest(input: {
const raw = await res.text().catch(() => ""); const raw = await res.text().catch(() => "");
const statusLine = `${res.status} ${res.statusText || ""}`.trim(); const statusLine = `${res.status} ${res.statusText || ""}`.trim();
// Show something useful even if raw is empty
let message = `Admin API error: ${statusLine}`; let message = `Admin API error: ${statusLine}`;
if (raw) { if (raw) {
@ -87,12 +94,9 @@ export async function submitCommissionRequest(input: {
} }
} }
// Log full body server-side for debugging (safe; this is server-only)
console.error("[submitCommissionRequest] upstream error", { statusLine, raw }); console.error("[submitCommissionRequest] upstream error", { statusLine, raw });
throw new Error(message); throw new Error(message);
} }
// Expected response: { id: string; createdAt: string }
return (await res.json()) as { id: string; createdAt: string }; return (await res.json()) as { id: string; createdAt: string };
} }

View File

@ -1,7 +1,7 @@
"use server"; "use server";
import type { PortfolioFilters } from "@/actions/portfolio/getPortfolioArtworksPage"; import type { PortfolioFilters } from "@/actions/portfolio/getPortfolioArtworksPage";
import { Prisma } from "@/generated/prisma/browser"; import type { Prisma } from "@/generated/prisma/browser";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
function coerceYear(y: PortfolioFilters["year"]) { function coerceYear(y: PortfolioFilters["year"]) {

View File

@ -12,6 +12,7 @@ export type PortfolioArtworkItem = {
id: string; id: string;
name: string; name: string;
altText: string | null; altText: string | null;
nsfw: boolean;
sortKey: number | null; sortKey: number | null;
year: number | null; year: number | null;
@ -107,13 +108,13 @@ export async function getPortfolioArtworksPage(args: {
.filter((y): y is number => typeof y === "number") .filter((y): y is number => typeof y === "number")
.sort((a, b) => b - a); .sort((a, b) => b - a);
// Segment logic (sortKey != null first, then null)
const inNullSegment = cursor?.afterSortKey === null; const inNullSegment = cursor?.afterSortKey === null;
const select = { const select = {
id: true, id: true,
name: true, name: true,
altText: true, altText: true,
nsfw: true,
year: true, year: true,
sortKey: true, sortKey: true,
file: { select: { fileKey: true } }, file: { select: { fileKey: true } },
@ -139,6 +140,7 @@ export async function getPortfolioArtworksPage(args: {
id: r.id, id: r.id,
name: r.name, name: r.name,
altText: r.altText ?? null, altText: r.altText ?? null,
nsfw: r.nsfw ?? false,
sortKey: r.sortKey ?? null, sortKey: r.sortKey ?? null,
year: r.year ?? null, year: r.year ?? null,
fileKey: r.file.fileKey, fileKey: r.file.fileKey,
@ -180,7 +182,6 @@ export async function getPortfolioArtworksPage(args: {
if (!last) { if (!last) {
return { items, nextCursor: null, total, years, albums }; return { items, nextCursor: null, total, years, albums };
} }
// last.sortKey can be null only in null-segment, which we are not in here.
if (last.sortKey == null) { if (last.sortKey == null) {
return { items, nextCursor: null, total, years, albums }; return { items, nextCursor: null, total, years, albums };
} }

View File

@ -0,0 +1,176 @@
"use server";
import type { Prisma } from "@/generated/prisma/browser";
import { prisma } from "@/lib/prisma";
export type Cursor = {
afterSortKey: number | null;
afterId: string;
} | null;
export type TaggedArtworkItem = {
id: string;
name: string;
altText: string | null;
nsfw: boolean;
sortKey: number | null;
year: number | null;
fileKey: string;
thumbW: number;
thumbH: number;
dominantHex: string;
};
type VariantPick = { type: string; width: number; height: number };
function pickVariant(variants: VariantPick[], type: string) {
return variants.find((v) => v.type === type) ?? null;
}
export async function getTaggedArtworksPage(args: {
take?: number;
cursor?: Cursor;
tagSlugs: string[];
onlyPublished?: boolean;
}): Promise<{
items: TaggedArtworkItem[];
nextCursor: Cursor;
total: number;
}> {
const { take = 60, cursor = null, tagSlugs, onlyPublished = true } = args;
const filteredSlugs = tagSlugs.map((s) => s.trim()).filter(Boolean);
if (filteredSlugs.length === 0) {
return { items: [], nextCursor: null, total: 0 };
}
const baseWhere: Prisma.ArtworkWhereInput = {
...(onlyPublished ? { published: true } : {}),
tags: { some: { slug: { in: filteredSlugs } } },
variants: { some: { type: "thumbnail" } },
};
const total = await prisma.artwork.count({ where: baseWhere });
const select = {
id: true,
name: true,
altText: true,
nsfw: true,
year: true,
sortKey: true,
file: { select: { fileKey: true } },
variants: {
where: { type: "thumbnail" },
select: { type: true, width: true, height: true },
take: 1,
},
colors: {
where: { type: "Vibrant" },
select: { color: { select: { hex: true } } },
take: 1,
},
} satisfies Prisma.ArtworkSelect;
type ArtworkRow = Prisma.ArtworkGetPayload<{ select: typeof select }>;
const mapRow = (r: ArtworkRow): TaggedArtworkItem | null => {
const thumb = pickVariant(r.variants, "thumbnail");
if (!thumb?.width || !thumb?.height) return null;
return {
id: r.id,
name: r.name,
altText: r.altText ?? null,
nsfw: r.nsfw ?? false,
sortKey: r.sortKey ?? null,
year: r.year ?? null,
fileKey: r.file.fileKey,
thumbW: thumb.width,
thumbH: thumb.height,
dominantHex: r.colors[0]?.color?.hex ?? "#999999",
};
};
let items: TaggedArtworkItem[] = [];
let nextCursor: Cursor = null;
const inNullSegment = cursor?.afterSortKey === null;
if (!inNullSegment) {
const whereA: Prisma.ArtworkWhereInput = {
AND: [baseWhere, { sortKey: { not: null } }],
};
if (cursor?.afterSortKey != null) {
const sk = Number(cursor.afterSortKey);
whereA.OR = [
{ sortKey: { gt: sk } },
{ AND: [{ sortKey: sk }, { id: { gt: cursor.afterId } }] },
];
}
const rowsA = await prisma.artwork.findMany({
where: whereA,
orderBy: [{ sortKey: "asc" }, { id: "asc" }],
take: Math.min(take, 200),
select,
});
items = rowsA.map(mapRow).filter((x): x is TaggedArtworkItem => x !== null);
if (items.length >= take) {
const last = items.at(-1);
if (!last || last.sortKey == null) {
return { items, nextCursor: null, total };
}
nextCursor = { afterSortKey: last.sortKey, afterId: last.id };
return { items, nextCursor, total };
}
const remaining = take - items.length;
const whereB: Prisma.ArtworkWhereInput = {
AND: [baseWhere, { sortKey: null }],
};
const rowsB = await prisma.artwork.findMany({
where: whereB,
orderBy: [{ id: "asc" }],
take: Math.min(remaining, 200),
select,
});
const more = rowsB.map(mapRow).filter((x): x is TaggedArtworkItem => x !== null);
items = items.concat(more);
const last = items[items.length - 1];
nextCursor =
items.length < take || !last
? null
: { afterSortKey: last.sortKey ?? null, afterId: last.id };
return { items, nextCursor, total };
}
const whereB: Prisma.ArtworkWhereInput = {
AND: [baseWhere, { sortKey: null }],
...(cursor ? { id: { gt: cursor.afterId } } : {}),
};
const rowsB = await prisma.artwork.findMany({
where: whereB,
orderBy: [{ id: "asc" }],
take: Math.min(take, 200),
select,
});
items = rowsB.map(mapRow).filter((x): x is TaggedArtworkItem => x !== null);
const last = items[items.length - 1];
nextCursor =
items.length < take || !last
? null
: { afterSortKey: null, afterId: last.id };
return { items, nextCursor, total };
}

View File

@ -1,4 +1,4 @@
import { ArrowLeftIcon } from "lucide-react"; import { ArrowLeftIcon, ChevronRightIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { import {
@ -30,14 +30,22 @@ function sortArtworks(a: SimpleArtwork, b: SimpleArtwork) {
} }
export default async function AnimalListPage() { export default async function AnimalListPage() {
const tags = await prisma.artTag.findMany({ const tags = await prisma.tag.findMany({
where: { showOnAnimalPage: true }, where: {
isVisible: true,
categoryLinks: {
some: { category: { name: "Animal Studies" }, showOnAnimalPage: true },
},
},
select: { select: {
id: true, id: true,
name: true, name: true,
slug: true, slug: true,
sortIndex: true, sortIndex: true,
parentId: true, categoryLinks: {
where: { category: { name: "Animal Studies" } },
select: { parentTagId: true },
},
artworks: { artworks: {
where: { where: {
published: true, published: true,
@ -50,10 +58,15 @@ export default async function AnimalListPage() {
orderBy: [{ sortIndex: "asc" }, { name: "asc" }], orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
}); });
const byId = new Map(tags.map((t) => [t.id, t])); const tagsWithParents = tags.map((t) => ({
const childrenByParentId = new Map<string, typeof tags>(); ...t,
parentId: t.categoryLinks[0]?.parentTagId ?? null,
}));
for (const t of tags) { const byId = new Map(tagsWithParents.map((t) => [t.id, t]));
const childrenByParentId = new Map<string, typeof tagsWithParents>();
for (const t of tagsWithParents) {
if (!t.parentId) continue; if (!t.parentId) continue;
const arr = childrenByParentId.get(t.parentId) ?? []; const arr = childrenByParentId.get(t.parentId) ?? [];
arr.push(t); arr.push(t);
@ -64,12 +77,12 @@ export default async function AnimalListPage() {
childrenByParentId.set(pid, arr.slice().sort(sortBySortIndexName)); childrenByParentId.set(pid, arr.slice().sort(sortBySortIndexName));
} }
const parents = tags const parents = tagsWithParents
.filter((t) => t.parentId === null) .filter((t) => t.parentId === null)
.slice() .slice()
.sort(sortBySortIndexName); .sort(sortBySortIndexName);
const orphans = tags const orphans = tagsWithParents
.filter((t) => t.parentId !== null && !byId.has(t.parentId)) .filter((t) => t.parentId !== null && !byId.has(t.parentId))
.slice() .slice()
.sort(sortBySortIndexName); .sort(sortBySortIndexName);
@ -82,16 +95,17 @@ export default async function AnimalListPage() {
} }
return ( return (
<ul className="space-y-1.5"> <ul className="space-y-1">
{list.map((a) => ( {list.map((a) => (
<li key={a.id}> <li key={a.id}>
<Link <Link
href={`/artworks/single/${a.id}?from=animal-index`} href={`/artworks/single/${a.id}?from=animal-index`}
className=" className="
inline-flex items-center gap-2 group flex w-full items-center gap-2
rounded-md px-2 py-1 rounded-md px-2 py-1.5
text-sm font-medium text-sm font-medium
hover:bg-muted hover:bg-muted/60
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
" "
> >
<span <span
@ -102,7 +116,7 @@ export default async function AnimalListPage() {
group-hover:translate-x-0.5 group-hover:translate-x-0.5
" "
> >
<ChevronRightIcon className="h-4 w-4" />
</span> </span>
<span className="leading-snug">{a.name}</span> <span className="leading-snug">{a.name}</span>
@ -149,7 +163,7 @@ export default async function AnimalListPage() {
</div> </div>
</header> </header>
<div className="space-y-6"> <div className="space-y-6 sm:space-y-4">
<Card> <Card>
<CardHeader className="space-y-1"> <CardHeader className="space-y-1">
<CardTitle className="text-base"> <CardTitle className="text-base">
@ -177,15 +191,17 @@ export default async function AnimalListPage() {
const isStandalone = children.length === 0; const isStandalone = children.length === 0;
return ( return (
<AccordionItem key={p.id} value={p.id} className="border-b"> <AccordionItem key={p.id} value={p.id} className="py-1 sm:py-1">
<AccordionTrigger <AccordionTrigger
className=" className="
py-4 py-4 sm:py-3
rounded-md px-2 -mx-2 rounded-md px-2 -mx-2
bg-hover text-hover-foreground dark:bg-hover dark:text-hover-foreground
transition-colors transition-colors
hover:bg-muted/60 hover:bg-hover/80 dark:hover:bg-hover/80
font-semibold
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
data-[state=open]:bg-muted/40 data-[state=open]:bg-muted/90 dark:data-[state=open]:bg-muted/90
" "
> >
<div className="flex w-full items-center justify-between pr-2"> <div className="flex w-full items-center justify-between pr-2">
@ -211,41 +227,44 @@ export default async function AnimalListPage() {
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="pb-5"> <AccordionContent className="pb-4 sm:pb-4">
{isStandalone ? ( {isStandalone ? (
<div className="rounded-md border p-4"> <div className="space-y-2">
<div className="mb-3 flex items-center justify-between"> <div className="flex items-center justify-between text-sm font-medium">
<div className="text-sm font-medium">Artworks</div> <span>Artworks</span>
<Badge variant="outline">{p.artworks.length}</Badge> <Badge variant="outline">{p.artworks.length}</Badge>
</div> </div>
<ArtworkList items={p.artworks} /> <div>
<ArtworkList items={p.artworks} />
</div>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4 sm:space-y-3">
{p.artworks.length > 0 ? ( {p.artworks.length > 0 ? (
<> <>
<div className="rounded-md border p-4"> <div className="space-y-2">
<div className="mb-3 flex items-center justify-between"> <div className="flex items-center justify-between text-sm font-medium">
<div className="text-sm font-medium">{/* Directly tagged */}</div> <span>Direct artworks</span>
<Badge variant="outline">{p.artworks.length}</Badge> <Badge variant="outline">{p.artworks.length}</Badge>
</div> </div>
<ArtworkList items={p.artworks} /> <div>
<ArtworkList items={p.artworks} />
</div>
</div> </div>
<Separator /> <Separator />
</> </>
) : null} ) : null}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:gap-2 sm:grid-cols-2">
{children.map((c) => ( {children.map((c) => (
<div key={c.id} className="rounded-md border p-4"> <div key={c.id} className="space-y-2 pt-3">
<div className="mb-3 flex items-center justify-between"> <div className="flex items-center justify-between text-sm font-medium">
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-sm font-medium">{c.name}</div> <div className="truncate">{c.name}</div>
</div> </div>
<Badge variant="outline">{c.artworks.length}</Badge> <Badge variant="outline">{c.artworks.length}</Badge>
</div> </div>
<ArtworkList items={c.artworks} /> <ArtworkList items={c.artworks} />
</div> </div>
))} ))}
@ -269,12 +288,12 @@ export default async function AnimalListPage() {
Tags whose parent is not visible (or not configured for the animal page). Tags whose parent is not visible (or not configured for the animal page).
</p> */} </p> */}
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-4 sm:space-y-2">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:gap-2 sm:grid-cols-2">
{orphans.map((t) => ( {orphans.map((t) => (
<div key={t.id} className="rounded-md border p-4"> <div key={t.id} className="space-y-2 pt-3">
<div className="mb-3 flex items-center justify-between"> <div className="flex items-center justify-between text-sm font-medium">
<div className="truncate text-sm font-medium">{t.name}</div> <div className="truncate">{t.name}</div>
<Badge variant="outline">{t.artworks.length}</Badge> <Badge variant="outline">{t.artworks.length}</Badge>
</div> </div>
<ArtworkList items={t.artworks} /> <ArtworkList items={t.artworks} />

View File

@ -17,17 +17,28 @@ function parseTagsParam(tags: string | string[] | undefined): string[] {
function expandSelectedWithChildren( function expandSelectedWithChildren(
selectedSlugs: string[], selectedSlugs: string[],
tagsForFilter: Array<{ tagsForFilter: Array<{
id: string;
slug: string; slug: string;
children: Array<{ slug: string }>; parentId: string | null;
}>, }>,
) { ) {
const bySlug = new Map(tagsForFilter.map((t) => [t.slug, t])); const bySlug = new Map(tagsForFilter.map((t) => [t.slug, t]));
const childrenByParentId = new Map<string, typeof tagsForFilter>();
for (const t of tagsForFilter) {
if (!t.parentId) continue;
const arr = childrenByParentId.get(t.parentId) ?? [];
arr.push(t);
childrenByParentId.set(t.parentId, arr);
}
const out = new Set(selectedSlugs); const out = new Set(selectedSlugs);
for (const slug of selectedSlugs) { for (const slug of selectedSlugs) {
const t = bySlug.get(slug); const t = bySlug.get(slug);
if (!t) continue; if (!t) continue;
for (const c of t.children ?? []) out.add(c.slug); const children = childrenByParentId.get(t.id) ?? [];
for (const c of children) out.add(c.slug);
} }
return Array.from(out); return Array.from(out);
@ -41,24 +52,31 @@ export default async function AnimalStudiesPage({
const { tags } = await searchParams; const { tags } = await searchParams;
const selectedTagSlugs = parseTagsParam(tags); const selectedTagSlugs = parseTagsParam(tags);
const tagsForFilter = await prisma.artTag.findMany({ const tagLinks = await prisma.tagCategory.findMany({
where: { showOnAnimalPage: true }, where: {
showOnAnimalPage: true,
category: { name: "Animal Studies" },
tag: { isVisible: true },
},
select: { select: {
id: true, parentTagId: true,
name: true, tag: {
slug: true, select: { id: true, name: true, slug: true, sortIndex: true },
sortIndex: true,
parentId: true,
parent: { select: { id: true, name: true, slug: true, sortIndex: true } },
children: {
where: { showOnAnimalPage: true },
select: { id: true, name: true, slug: true, sortIndex: true, parentId: true },
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
}, },
}, },
orderBy: [{ sortIndex: "asc" }, { name: "asc" }], orderBy: [{ tag: { sortIndex: "asc" } }, { tag: { name: "asc" } }],
}); });
const tagsForFilter = tagLinks.map((link) => ({
id: link.tag.id,
name: link.tag.name,
slug: link.tag.slug,
sortIndex: link.tag.sortIndex,
parentId: link.parentTagId,
parent: null,
children: [],
}));
const expandedTagSlugs = expandSelectedWithChildren(selectedTagSlugs, tagsForFilter); const expandedTagSlugs = expandSelectedWithChildren(selectedTagSlugs, tagsForFilter);
return ( return (

View File

@ -1,14 +1,20 @@
import ArtworkMetaCard from "@/components/artworks/ArtworkMetaCard"; import ArtworkMetaCard from "@/components/artworks/ArtworkMetaCard";
import ArtworkTimelapseViewer from "@/components/artworks/ArtworkTimelapseViewer"; import ArtworkTimelapseViewer from "@/components/artworks/ArtworkTimelapseViewer";
import { ContextBackButton } from "@/components/artworks/ContextBackButton"; import { ContextBackButton } from "@/components/artworks/ContextBackButton";
import NsfwConsentDialog from "@/components/nsfw/NsfwConsentDialog";
import NsfwImage from "@/components/nsfw/NsfwImage";
import NsfwLink from "@/components/nsfw/NsfwLink";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { PlayCircle } from "lucide-react"; import { PlayCircle } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
export default async function SingleArtworkPage({ params }: { params: { id: string }; searchParams: Record<string, string | string[] | undefined>; }) { export default async function SingleArtworkPage({
params,
}: {
params: { id: string };
searchParams: Record<string, string | string[] | undefined>;
}) {
const { id } = await params; const { id } = await params;
const artwork = await prisma.artwork.findUnique({ const artwork = await prisma.artwork.findUnique({
where: { where: {
@ -24,46 +30,58 @@ export default async function SingleArtworkPage({ params }: { params: { id: stri
tags: true, tags: true,
variants: true, variants: true,
timelapse: { where: { enabled: true } }, timelapse: { where: { enabled: true } },
} },
}) });
if (!artwork) return <div>Artwork with this ID could not be found</div> if (!artwork) return <div>Artwork with this ID could not be found</div>;
const { width, height } = artwork.variants.find((v) => v.type === "resized") ?? { width: 0, height: 0 } const { width, height } = artwork.variants.find(
(v) => v.type === "resized",
) ?? { width: 0, height: 0 };
const colors = const colors =
artwork.colors?.map((c) => c.color?.hex).filter((hex): hex is string => Boolean(hex)) ?? [] artwork.colors
?.map((c) => c.color?.hex)
.filter((hex): hex is string => Boolean(hex)) ?? [];
const gradientColors = colors.length const gradientColors = colors.length
? colors.join(", ") ? colors.join(", ")
: "rgba(0,0,0,0.1), rgba(0,0,0,0.03)" : "rgba(0,0,0,0.1), rgba(0,0,0,0.03)";
return ( return (
<div className="px-8 py-4"> <div className="px-4 sm:px-8 py-4">
<NsfwConsentDialog hasNsfw={Boolean(artwork.nsfw)} />
<div className="relative w-full min-h-10 flex items-center mb-4"> <div className="relative w-full min-h-10 flex items-center mb-4">
<div className="z-10"><ContextBackButton /></div> <div className="z-10 hidden sm:block">
<ContextBackButton />
</div>
{artwork.name ? ( {artwork.name ? (
<div className="pointer-events-none absolute left-1/2 -translate-x-1/2 text-center"> <div className="w-full text-center sm:pointer-events-none sm:absolute sm:left-1/2 sm:-translate-x-1/2">
<div className="pointer-events-auto"><h1 className="text-2xl font-bold mb-4 py-4">{artwork.name}</h1></div> <div className="sm:pointer-events-auto">
<h1 className="text-xl sm:text-2xl font-bold mb-2 sm:mb-4 py-2 sm:py-4 px-2 sm:px-0 wrap-break-word">
{artwork.name}
</h1>
</div>
</div> </div>
) : null} ) : null}
</div> </div>
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<div className="group rounded-lg border overflow-hidden hover:shadow-lg transition-shadow bg-background relative"> <div className="group rounded-lg border overflow-hidden hover:shadow-lg transition-shadow bg-background relative">
<div className="relative w-full bg-muted items-center justify-center" <div
className="relative w-full bg-muted items-center justify-center"
style={{ aspectRatio: "4 / 3" }} style={{ aspectRatio: "4 / 3" }}
> >
<Link href={`/raw/${artwork.id}`}> <NsfwLink href={`/raw/${artwork.id}`} nsfw={Boolean(artwork.nsfw)}>
<Image <NsfwImage
src={`/api/image/resized/${artwork.file.fileKey}.webp`} src={`/api/image/resized/${artwork.file.fileKey}.webp`}
alt={artwork.altText || "Artwork"} alt={artwork.altText || "Artwork"}
fill={!width || !height} fill={!width || !height}
width={width} width={width}
height={height} height={height}
nsfw={Boolean(artwork.nsfw)}
className={cn("object-cover transition duration-300")} className={cn("object-cover transition duration-300")}
/> />
</Link> </NsfwLink>
</div> </div>
</div> </div>
{artwork.timelapse?.enabled ? ( {artwork.timelapse?.enabled ? (
@ -94,7 +112,10 @@ export default async function SingleArtworkPage({ params }: { params: { id: stri
tags={artwork.tags} tags={artwork.tags}
/> />
</div> </div>
<div className="w-full flex justify-center sm:hidden">
<ContextBackButton className="mx-auto flex justify-center" />
</div>
</div> </div>
</div > </div>
); );
} }

View File

@ -0,0 +1,48 @@
import TaggedGallery from "@/components/portfolio/TaggedGallery";
import { prisma } from "@/lib/prisma";
function parseTagsParam(tags: string | string[] | undefined): string[] {
if (!tags) return [];
const raw = Array.isArray(tags) ? tags.join(",") : tags;
return raw
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
export default async function TaggedPortfolioPage({
searchParams,
}: {
searchParams: { tags?: string | string[] };
}) {
const { tags } = await searchParams;
const selectedTagSlugs = parseTagsParam(tags);
const tagsSelected = selectedTagSlugs.length
? await prisma.tag.findMany({
where: { slug: { in: selectedTagSlugs } },
select: { id: true, name: true, slug: true },
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})
: [];
return (
<div className="mx-auto w-full max-w-6xl px-4 py-8">
<header className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
{tagsSelected.length ? (
<div className="flex flex-wrap gap-2">
List of artworks tagged with:
{tagsSelected.map((t) => (
<span key={t.name.toLowerCase()}> {t.name.toLowerCase()}</span>
))}
</div>
) : "No tags selected"}
</h1>
</div>
</header>
<TaggedGallery tagSlugs={selectedTagSlugs} />
</div>
);
}

View File

@ -1,30 +1,97 @@
import { CommissionCard } from "@/components/commissions/CommissionCard"; import { CommissionCard } from "@/components/commissions/CommissionCard";
import { CommissionCustomCard } from "@/components/commissions/CommissionCustomCard";
import CommissionGuidelines from "@/components/commissions/CommissionGuidelines"; import CommissionGuidelines from "@/components/commissions/CommissionGuidelines";
import { CommissionOrderForm } from "@/components/commissions/CommissionOrderForm"; import { CommissionOrderForm } from "@/components/commissions/CommissionOrderForm";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import Image from "next/image";
export default async function CommissionsPage() { export default async function CommissionsPage() {
const commissions = await prisma.commissionType.findMany({ const [commissions, customCards, guidelines] = await Promise.all([
include: { prisma.commissionType.findMany({
options: { include: { option: true }, orderBy: { sortIndex: "asc" } }, include: {
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } }, options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
customInputs: { include: { customInput: true }, orderBy: { sortIndex: "asc" } }, extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
}, customInputs: {
orderBy: [{ sortIndex: "asc" }, { name: "asc" }], include: { customInput: true },
}) orderBy: { sortIndex: "asc" },
},
tags: true,
},
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
}),
prisma.commissionCustomCard.findMany({
where: { isVisible: true },
include: {
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
tags: true,
},
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
}),
prisma.commissionGuidelines.findFirst({
where: { isActive: true },
orderBy: { createdAt: "desc" },
select: { exampleImageUrl: true },
}),
]);
return ( return (
<div className="mx-auto w-full max-w-6xl px-4 py-8 flex flex-col gap-8"> <div className="mx-auto w-full max-w-6xl px-4 py-8 flex flex-col gap-8">
<h1 className="text-3xl font-bold">Commission Pricing</h1> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<h1 className="text-3xl font-bold">Commission Pricing</h1>
<div className="flex flex-wrap gap-3">
<Button asChild>
<a href="#commission-request-form">Get to the request form</a>
</Button>
{guidelines?.exampleImageUrl ? (
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary">View type example</Button>
</DialogTrigger>
<DialogContent className="flex w-auto! max-w-[95vw]! flex-col p-4 sm:p-6">
<DialogHeader className="sr-only">
<DialogTitle>Commission example</DialogTitle>
</DialogHeader>
<div className="flex max-h-[85vh] max-w-[85vw] items-center justify-center rounded-xl border-border/60 bg-muted p-2 shadow-2xl">
<Image
src={guidelines.exampleImageUrl}
alt="Commission example"
width={1600}
height={1200}
sizes="85vw"
className="h-auto max-h-[85vh] w-auto max-w-[85vw] rounded-lg object-contain"
/>
</div>
</DialogContent>
</Dialog>
) : null}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 items-start"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 items-start">
{commissions.map((commission) => ( {commissions.map((commission) => (
<CommissionCard key={commission.id} commission={commission} /> <CommissionCard key={commission.id} commission={commission} />
))} ))}
{customCards.map((card) => (
<CommissionCustomCard key={card.id} card={card} />
))}
<CommissionGuidelines /> <CommissionGuidelines />
</div> </div>
<hr /> <hr />
<h2 className="text-2xl font-semibold">Request a Commission</h2> <h2
<CommissionOrderForm types={commissions} /> id="commission-request-form"
className="text-2xl font-semibold scroll-mt-24"
>
Request a Commission
</h2>
<CommissionOrderForm types={commissions} customCards={customCards} />
</div> </div>
); );
} }

View File

@ -0,0 +1,125 @@
import { Badge } from "@/components/ui/badge";
import { prisma } from "@/lib/prisma";
const statusStyles: Record<string, string> = {
ACCEPTED:
"bg-sky-500/20 text-sky-700 border-sky-500/40 dark:bg-sky-500/15 dark:text-sky-300 dark:border-sky-500/30",
INPROGRESS:
"bg-amber-500/20 text-amber-700 border-amber-500/40 dark:bg-amber-500/15 dark:text-amber-300 dark:border-amber-500/30",
COMPLETED:
"bg-emerald-500/20 text-emerald-700 border-emerald-500/40 dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-500/30",
};
const statusLabels: Record<string, string> = {
ACCEPTED: "Accepted",
INPROGRESS: "In progress",
COMPLETED: "Completed",
};
export default async function CommissionStatusPage() {
const [queueItems, doneItems] = await Promise.all([
prisma.commissionRequest.findMany({
where: { status: { in: ["ACCEPTED", "INPROGRESS"] } },
include: { type: true, option: true, extras: true, customCard: true },
orderBy: { createdAt: "desc" },
}),
prisma.commissionRequest.findMany({
where: { status: "COMPLETED" },
include: { type: true, option: true, extras: true, customCard: true },
orderBy: { createdAt: "desc" },
}),
]);
return (
<div className="mx-auto w-full max-w-6xl px-4 py-8 flex flex-col gap-8">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold">Commission Status</h1>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<section className="rounded-2xl border border-border/60 bg-muted/20 p-4 sm:p-6">
<div className="mb-4">
<h2 className="text-xl font-semibold">Commissions Queue</h2>
<p className="text-sm text-muted-foreground">Accepted and in progress</p>
</div>
<div className="flex flex-col gap-3">
{queueItems.length > 0 ? (
queueItems.map((item) => (
<div
key={item.id}
className="rounded-xl border border-border/60 bg-background p-4 shadow-sm"
>
<div className="font-semibold text-lg">{item.customerName}</div>
<div className="mt-2 flex flex-wrap gap-2">
<Badge variant="outline">
{item.customCard?.name ?? item.type?.name ?? "Custom"}
</Badge>
<Badge variant="outline">
{item.option?.name ?? "Base option"}
</Badge>
{item.extras.map((extra) => (
<Badge key={extra.id} variant="secondary">
{extra.name}
</Badge>
))}
{item.extras.length === 0 ? (
<Badge variant="secondary">No extras</Badge>
) : null}
<Badge className={statusStyles[item.status] ?? ""}>
{statusLabels[item.status] ?? item.status}
</Badge>
</div>
</div>
))
) : (
<div className="rounded-xl border border-dashed border-border/60 bg-background p-4 text-sm text-muted-foreground">
No public items yet.
</div>
)}
</div>
</section>
<section className="rounded-2xl border border-border/60 bg-muted/20 p-4 sm:p-6">
<div className="mb-4">
<h2 className="text-xl font-semibold">Done</h2>
</div>
<div className="flex flex-col gap-3">
{doneItems.length > 0 ? (
doneItems.map((item) => (
<div
key={item.id}
className="rounded-xl border border-border/60 bg-background p-4 shadow-sm"
>
<div className="font-semibold text-lg">{item.customerName}</div>
<div className="mt-2 flex flex-wrap gap-2">
<Badge variant="outline">
{item.customCard?.name ?? item.type?.name ?? "Custom"}
</Badge>
<Badge variant="outline">
{item.option?.name ?? "Base option"}
</Badge>
{item.extras.map((extra) => (
<Badge key={extra.id} variant="secondary">
{extra.name}
</Badge>
))}
{item.extras.length === 0 ? (
<Badge variant="secondary">No extras</Badge>
) : null}
<Badge className={statusStyles[item.status] ?? ""}>
{statusLabels[item.status] ?? item.status}
</Badge>
</div>
</div>
))
) : (
<div className="rounded-xl border border-dashed border-border/60 bg-background p-4 text-sm text-muted-foreground">
No completed items yet.
</div>
)}
</div>
</section>
</div>
</div>
);
}

View File

@ -19,29 +19,6 @@ export default function Home() {
<SocialLinks /> <SocialLinks />
</div> </div>
</div> </div>
{/* Section Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
{/* <p>
If you want to commission me you can find all the information you need under following link: <a href="https://linktr.ee/gaertan" target="_blank">Linktree</a>
</p> */}
{/* {sections.map((section) => (
<Link href={section.href} key={section.title}>
<Card className="hover:shadow-xl transition-shadow group">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<section.icon className="w-5 h-5 text-muted-foreground group-hover:text-primary" />
{section.title}
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
{section.description}
</CardContent>
</Card>
</Link>
))} */}
</div>
</div > </div >
); );
} }

View File

@ -6,8 +6,6 @@ export default async function TosPage() {
orderBy: [{ version: "desc" }], orderBy: [{ version: "desc" }],
}) })
// console.log(tos?.markdown)
return ( return (
<div className="mx-auto w-full max-w-6xl px-4 py-8"> <div className="mx-auto w-full max-w-6xl px-4 py-8">
<div className="markdown"> <div className="markdown">

View File

@ -24,7 +24,7 @@ export async function GET(_req: NextRequest, context: { params: Promise<{ key: s
headers: { headers: {
"Content-Type": contentType, "Content-Type": contentType,
"Cache-Control": "public, max-age=3600", "Cache-Control": "public, max-age=3600",
"Content-Disposition": "inline", // use 'attachment' to force download "Content-Disposition": "inline",
}, },
}); });
} catch (err) { } catch (err) {

View File

@ -48,119 +48,33 @@
--color-hover-foreground: var(--hover-foreground); --color-hover-foreground: var(--hover-foreground);
} }
/* :root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
} */
:root { :root {
--radius: 0.625rem; --radius: 0.625rem;
--background: oklch(0.985 0.012 85);
/* Light: warm paper + graphite */ --foreground: oklch(0.18 0.02 35);
--background: oklch(0.985 0.012 85); /* warm off-white */ --card: oklch(0.992 0.008 85);
--foreground: oklch(0.18 0.02 35); /* graphite */
--card: oklch(0.992 0.008 85); /* slightly lifted paper */
--card-foreground: var(--foreground); --card-foreground: var(--foreground);
--popover: oklch(0.992 0.008 85); --popover: oklch(0.992 0.008 85);
--popover-foreground: var(--foreground); --popover-foreground: var(--foreground);
/* Primary: deep ink / indigo (artist-y but still neutral enough) */
--primary: oklch(0.32 0.06 260); --primary: oklch(0.32 0.06 260);
--primary-foreground: oklch(0.985 0.012 85); --primary-foreground: oklch(0.985 0.012 85);
/* Secondary/muted/accent: warm washes */
--secondary: oklch(0.96 0.015 85); --secondary: oklch(0.96 0.015 85);
--secondary-foreground: oklch(0.22 0.02 35); --secondary-foreground: oklch(0.22 0.02 35);
--muted: oklch(0.955 0.012 85); --muted: oklch(0.955 0.012 85);
--muted-foreground: oklch(0.46 0.02 35); --muted-foreground: oklch(0.46 0.02 35);
--accent: oklch(0.95 0.02 110);
--accent: oklch(0.95 0.02 110); /* subtle “wash” */
--accent-foreground: oklch(0.22 0.02 35); --accent-foreground: oklch(0.22 0.02 35);
--destructive: oklch(0.58 0.22 27.325); --destructive: oklch(0.58 0.22 27.325);
--border: oklch(0.90 0.02 85);
--border: oklch(0.90 0.02 85); /* warm border */
--input: oklch(0.90 0.02 85); --input: oklch(0.90 0.02 85);
--ring: oklch(0.55 0.07 260); /* ties to primary */ --ring: oklch(0.55 0.07 260);
--hover: oklch(0.94 0.015 255);
--hover: oklch(0.94 0.015 255); /* subtle cool lift */
--hover-foreground: var(--foreground); --hover-foreground: var(--foreground);
/* charts can stay, or we can harmonize later */
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704); --chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392); --chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429); --chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08); --chart-5: oklch(0.769 0.188 70.08);
/* sidebar inherits the same “paper” idea */
--sidebar: oklch(0.975 0.012 85); --sidebar: oklch(0.975 0.012 85);
--sidebar-foreground: var(--foreground); --sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary); --sidebar-primary: var(--primary);
@ -172,36 +86,40 @@
} }
.dark { .dark {
/* Inky navy background (clearly not neutral) */ --background: oklch(0.2223 0.0060 271.1393);
--background: oklch(15.774% 0.03835 263.588); --foreground: oklch(0.9551 0 0);
--foreground: oklch(0.95 0.012 85); --card: oklch(0.2568 0.0076 274.6528);
--card-foreground: oklch(0.9551 0 0);
/* Surfaces */ --popover: oklch(0.2568 0.0076 274.6528);
--card: oklch(0.155 0.03 255); --popover-foreground: oklch(0.9551 0 0);
--card-foreground: var(--foreground); --primary: oklch(0.6132 0.2294 291.7437);
--primary-foreground: oklch(1.0000 0 0);
--popover: oklch(0.155 0.03 255); --secondary: oklch(0.2940 0.0130 272.9312);
--popover-foreground: var(--foreground); --secondary-foreground: oklch(0.9551 0 0);
--muted: oklch(0.2940 0.0130 272.9312);
/* Primary accent stays “artist ink” */ --muted-foreground: oklch(0.7058 0 0);
--primary: oklch(0.78 0.11 255); --accent: oklch(0.2795 0.0368 260.0310);
--primary-foreground: oklch(0.13 0.03 255); --accent-foreground: oklch(0.7857 0.1153 246.6596);
--destructive: oklch(0.7106 0.1661 22.2162);
--secondary: oklch(0.19 0.025 255); --destructive-foreground: oklch(1.0000 0 0);
--secondary-foreground: var(--foreground); --border: oklch(0.3289 0.0092 268.3843);
--input: oklch(0.3289 0.0092 268.3843);
--muted: oklch(0.19 0.025 255); --ring: oklch(0.6132 0.2294 291.7437);
--muted-foreground: oklch(0.74 0.02 85); --hover: oklch(0.34 0.02 270);
--accent: oklch(0.22 0.03 110);
--accent-foreground: var(--foreground);
--border: oklch(0.40 0.03 255 / 55%);
--input: oklch(0.40 0.03 255 / 65%);
--ring: oklch(0.65 0.10 255);
--hover: oklch(28.783% 0.03139 250.817);
--hover-foreground: var(--foreground); --hover-foreground: var(--foreground);
--chart-1: oklch(0.8003 0.1821 151.7110);
--chart-2: oklch(0.6132 0.2294 291.7437);
--chart-3: oklch(0.8077 0.1035 19.5706);
--chart-4: oklch(0.6691 0.1569 260.1063);
--chart-5: oklch(0.7058 0 0);
--sidebar: oklch(0.2011 0.0039 286.0396);
--sidebar-foreground: oklch(0.9551 0 0);
--sidebar-primary: oklch(0.6132 0.2294 291.7437);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.2940 0.0130 272.9312);
--sidebar-accent-foreground: oklch(0.6132 0.2294 291.7437);
--sidebar-border: oklch(0.3289 0.0092 268.3843);
--sidebar-ring: oklch(0.6132 0.2294 291.7437);
} }
.markdown { .markdown {
@ -260,10 +178,6 @@
@apply line-through text-muted-foreground; @apply line-through text-muted-foreground;
} }
/* div:hover {
border-color: var(--hover-border-color);
} */
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
@ -276,7 +190,7 @@
} }
.dark body { .dark body {
background-image: background-image:
radial-gradient(1200px 700px at 15% -10%, oklch(0.55 0.14 255 / 16%), transparent 60%), radial-gradient(1200px 700px at 15% -10%, oklch(0.35 0.06 35 / 10%), transparent 60%),
radial-gradient(900px 600px at 85% 0%, oklch(0.50 0.12 110 / 10%), transparent 55%); radial-gradient(900px 600px at 85% 0%, oklch(0.30 0.05 255 / 6%), transparent 55%);
} }
} }

View File

@ -19,6 +19,11 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Gaertan Art", title: "Gaertan Art",
description: "Portfolio, Artworks and Commission Requests", description: "Portfolio, Artworks and Commission Requests",
alternates: {
types: {
"application/rss+xml": "/rss.xml",
},
},
}; };
export default function RootLayout({ export default function RootLayout({

71
src/app/rss.xml/route.ts Normal file
View File

@ -0,0 +1,71 @@
import { prisma } from "@/lib/prisma";
const BASE_URL = `${process.env.FEED_URL}`
function escapeXml(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
export async function GET() {
const items = await prisma.artwork.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
take: 10,
select: {
id: true,
name: true,
description: true,
altText: true,
createdAt: true,
updatedAt: true,
},
});
const lastBuildDate =
items[0]?.updatedAt?.toUTCString() ?? new Date().toUTCString();
const itemXml = items
.map((item) => {
const title = escapeXml(item.name || "Artwork");
const description = escapeXml(
item.description || item.altText || item.name || "Artwork"
);
const link = `${BASE_URL}/artworks/single/${item.id}`;
const pubDate = item.createdAt.toUTCString();
return [
"<item>",
`<title>${title}</title>`,
`<link>${link}</link>`,
`<guid isPermaLink="true">${link}</guid>`,
`<description>${description}</description>`,
`<pubDate>${pubDate}</pubDate>`,
"</item>",
].join("");
})
.join("");
const xml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<rss version="2.0">',
"<channel>",
"<title>Gaertan Art - Latest Artworks</title>",
`<link>${BASE_URL}</link>`,
"<description>Ten newest artworks from Gaertan Art.</description>",
`<lastBuildDate>${lastBuildDate}</lastBuildDate>`,
itemXml,
"</channel>",
"</rss>",
].join("");
return new Response(xml, {
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
},
});
}

View File

@ -3,12 +3,20 @@ import {
siLinktree, siLinktree,
siMastodon, siMastodon,
siPaypal, siPaypal,
siRss,
siTelegram, siTelegram,
siTwitch, siTwitch,
type SimpleIcon, type SimpleIcon,
} from "simple-icons"; } from "simple-icons";
type SocialKey = "paypal" | "telegram" | "mastodon" | "bluesky" | "linktree" | "twitch"; type SocialKey =
| "paypal"
| "telegram"
| "mastodon"
| "bluesky"
| "linktree"
| "twitch"
| "rss";
const SOCIALS: Record< const SOCIALS: Record<
SocialKey, SocialKey,
@ -43,7 +51,12 @@ const SOCIALS: Record<
label: "Twitch", label: "Twitch",
icon: siTwitch, icon: siTwitch,
href: "https://www.twitch.tv/gaertan_art", href: "https://www.twitch.tv/gaertan_art",
} },
rss: {
label: "RSS",
icon: siRss,
href: `/rss.xml`,
},
}; };
function BrandSvg({ icon }: { icon: SimpleIcon }) { function BrandSvg({ icon }: { icon: SimpleIcon }) {
@ -54,13 +67,22 @@ function BrandSvg({ icon }: { icon: SimpleIcon }) {
className="h-5 w-5 fill-current" className="h-5 w-5 fill-current"
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"
// biome-ignore lint: lint/security/noDangerouslySetInnerHtml
dangerouslySetInnerHTML={{ __html: icon.svg }} dangerouslySetInnerHTML={{ __html: icon.svg }}
/> />
); );
} }
export function SocialLinks({ export function SocialLinks({
items = ["paypal", "telegram", "mastodon", "bluesky", "linktree", "twitch"], items = [
"paypal",
"telegram",
"mastodon",
"bluesky",
"linktree",
"twitch",
"rss",
],
size = "md", size = "md",
}: { }: {
items?: SocialKey[]; items?: SocialKey[];

View File

@ -27,9 +27,6 @@ export default function ArtworkTimelapseViewer({
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
// IMPORTANT:
// This assumes your existing `/api/image/[...key]` can stream arbitrary S3 keys.
// If your route expects a different format, adjust this in one place.
const src = `/api/image/${encodeURI(timelapse.s3Key)}`; const src = `/api/image/${encodeURI(timelapse.s3Key)}`;
// Minimal empty captions track (satisfies jsx-a11y/media-has-caption) // Minimal empty captions track (satisfies jsx-a11y/media-has-caption)
@ -46,7 +43,6 @@ export default function ArtworkTimelapseViewer({
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
</DialogHeader> </DialogHeader>
{/* Only render video when open (prevents unnecessary network / CPU). */}
{open ? ( {open ? (
<div className="space-y-2"> <div className="space-y-2">
<video <video
@ -61,7 +57,7 @@ export default function ArtworkTimelapseViewer({
</video> </video>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{timelapse.fileName ? timelapse.fileName : timelapse.s3Key} {/* {timelapse.fileName ? timelapse.fileName : timelapse.s3Key} */}
</div> </div>
</div> </div>
) : null} ) : null}

View File

@ -10,7 +10,7 @@ const FROM_TO_PATH: Record<string, string> = {
"animal-index": "/artworks/animalstudies/index" "animal-index": "/artworks/animalstudies/index"
}; };
export function ContextBackButton() { export function ContextBackButton({ className }: { className?: string }) {
const router = useRouter(); const router = useRouter();
const sp = useSearchParams(); const sp = useSearchParams();
const from = sp.get("from") ?? ""; const from = sp.get("from") ?? "";
@ -19,7 +19,7 @@ export function ContextBackButton() {
if (!target) return null; if (!target) return null;
return ( return (
<div className="w-full max-w-xl"> <div className={["w-full max-w-xl", className].filter(Boolean).join(" ")}>
<Link <Link
href={target} href={target}
className={[ className={[

View File

@ -26,6 +26,7 @@ export default function RawCloseButton({ targetHref }: RawCloseButtonProps) {
onClick={() => router.push(targetHref)} onClick={() => router.push(targetHref)}
className="absolute top-4 right-4 z-50 rounded-md bg-background/80 p-2 hover:bg-background/60 transition" className="absolute top-4 right-4 z-50 rounded-md bg-background/80 p-2 hover:bg-background/60 transition"
title="Close full view (ESC)" title="Close full view (ESC)"
type="button"
> >
<X className="w-6 h-6" /> <X className="w-6 h-6" />
</button> </button>

View File

@ -25,7 +25,6 @@ type Tag = {
slug: string; slug: string;
sortIndex: number; sortIndex: number;
parentId: string | null; parentId: string | null;
// these may exist, but we do NOT rely on them:
parent?: { id: string; name: string; slug: string; sortIndex: number } | null; parent?: { id: string; name: string; slug: string; sortIndex: number } | null;
children?: { id: string; name: string; slug: string; sortIndex: number; parentId: string | null }[]; children?: { id: string; name: string; slug: string; sortIndex: number; parentId: string | null }[];
}; };
@ -65,7 +64,6 @@ export default function TagFilterDialog({
const byId = useMemo(() => new Map(tags.map((t) => [t.id, t])), [tags]); const byId = useMemo(() => new Map(tags.map((t) => [t.id, t])), [tags]);
// Build children mapping from the flat list: parentId -> Tag[]
const childrenByParentId = useMemo(() => { const childrenByParentId = useMemo(() => {
const map = new Map<string, Tag[]>(); const map = new Map<string, Tag[]>();
for (const t of tags) { for (const t of tags) {
@ -74,7 +72,6 @@ export default function TagFilterDialog({
arr.push(t); arr.push(t);
map.set(t.parentId, arr); map.set(t.parentId, arr);
} }
// sort each child list
for (const [k, arr] of map) { for (const [k, arr] of map) {
map.set(k, arr.slice().sort(sortTags)); map.set(k, arr.slice().sort(sortTags));
} }
@ -101,7 +98,6 @@ export default function TagFilterDialog({
const s = new Set(prev); const s = new Set(prev);
if (next) { if (next) {
s.add(parent.slug); s.add(parent.slug);
// when selecting parent, remove child selections (redundant)
for (const c of children) s.delete(c.slug); for (const c of children) s.delete(c.slug);
} else { } else {
s.delete(parent.slug); s.delete(parent.slug);
@ -188,9 +184,6 @@ export default function TagFilterDialog({
/> />
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate font-medium">{p.name}</div> <div className="truncate font-medium">{p.name}</div>
{/* <div className="text-xs text-muted-foreground">
{children.length ? "Parent tag" : "Tag"}
</div> */}
</div> </div>
</Label> </Label>
@ -231,11 +224,6 @@ export default function TagFilterDialog({
{orphanChildren.length ? ( {orphanChildren.length ? (
<div className="rounded-lg border p-4"> <div className="rounded-lg border p-4">
{/* <div className="mb-2 font-medium">Other tags</div> */}
{/* <div className="mb-3 text-xs text-muted-foreground">
These tags are not currently assigned to a visible parent.
</div> */}
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{orphanChildren.map((t) => { {orphanChildren.map((t) => {
const checked = selectedSet.has(t.slug); const checked = selectedSet.has(t.slug);

View File

@ -1,52 +1,43 @@
"use client" "use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button";
import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma/client" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type {
CommissionExtra,
CommissionOption,
CommissionType,
CommissionTypeExtra,
CommissionTypeOption,
Tag,
} from "@/generated/prisma/client";
import Link from "next/link";
type CommissionTypeWithItems = CommissionType & { type CommissionTypeWithItems = CommissionType & {
options: (CommissionTypeOption & { options: (CommissionTypeOption & {
option: CommissionOption | null option: CommissionOption | null;
})[] })[];
extras: (CommissionTypeExtra & { extras: (CommissionTypeExtra & {
extra: CommissionExtra | null extra: CommissionExtra | null;
})[] })[];
} tags: Tag[];
};
export function CommissionCard({ commission }: { commission: CommissionTypeWithItems }) {
// const [open, setOpen] = useState(false)
export function CommissionCard({
commission,
}: {
commission: CommissionTypeWithItems;
}) {
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<Card className="flex flex-col flex-1"> <Card className="flex flex-col flex-1">
<CardHeader> <CardHeader>
<CardTitle className="text-xl font-bold">{commission.name}</CardTitle> <CardTitle className="text-xl font-bold">{commission.name}</CardTitle>
<p className="text-muted-foreground text-sm">{commission.description}</p> <p className="text-muted-foreground text-sm">
{commission.description}
</p>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col justify-start gap-4"> <CardContent className="flex flex-col flex-1 justify-start gap-4">
{/* {examples && examples.length > 0 && (
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger className="text-sm underline text-muted-foreground">
{open ? "Hide Examples" : "See Examples"}
</CollapsibleTrigger>
<CollapsibleContent asChild>
<div className="overflow-hidden transition-all data-[state=closed]:max-h-0 data-[state=open]:max-h-[300px]">
<div className="flex gap-2 mt-2 overflow-x-auto">
{examples.map((src, idx) => (
<Image
key={src + idx}
src={src}
width={100}
height={100}
alt={`${type.name} example ${idx + 1}`}
className="h-24 w-auto rounded border"
/>
))}
</div>
</div>
</CollapsibleContent>
</Collapsible>
)} */}
<div> <div>
<h4 className="font-semibold">Options</h4> <h4 className="font-semibold">Options</h4>
<ul className="pl-4 list-disc"> <ul className="pl-4 list-disc">
@ -66,7 +57,9 @@ export function CommissionCard({ commission }: { commission: CommissionTypeWithI
</div> </div>
<div> <div>
{commission.extras.length > 0 && <h4 className="font-semibold">Extras</h4>} {commission.extras.length > 0 && (
<h4 className="font-semibold">Extras</h4>
)}
<ul className="pl-4 list-disc"> <ul className="pl-4 list-disc">
{commission.extras.map((extra) => ( {commission.extras.map((extra) => (
<li key={extra.id}> <li key={extra.id}>
@ -82,16 +75,21 @@ export function CommissionCard({ commission }: { commission: CommissionTypeWithI
))} ))}
</ul> </ul>
</div> </div>
{/* <div className="flex flex-wrap gap-2">
{commission.extras.map((extra) => (
<Badge variant="outline" key={extra.id}>
{extra.extra?.name}
</Badge>
))}
</div> */}
</CardContent> </CardContent>
{commission.tags.length > 0 ? (
<div className="mt-auto px-6 pb-6">
<Link
href={`/portfolio/tagged?tags=${encodeURIComponent(
commission.tags.map((t) => t.slug).join(","),
)}`}
>
<Button variant="secondary" className="w-full">
View example artworks
</Button>
</Link>
</div>
) : null}
</Card> </Card>
</div> </div>
) );
} }

View File

@ -0,0 +1,167 @@
"use client";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import type { Tag } from "@/generated/prisma/client";
import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
type CustomCardOption = {
id: string;
price: number | null;
pricePercent: number | null;
priceRange: string | null;
option: { name: string } | null;
};
type CustomCardExtra = {
id: string;
price: number | null;
pricePercent: number | null;
priceRange: string | null;
extra: { name: string } | null;
};
export type CommissionCustomCardWithItems = {
id: string;
name: string;
description: string | null;
referenceImageUrl: string | null;
isSpecialOffer: boolean;
tags: Tag[];
options: CustomCardOption[];
extras: CustomCardExtra[];
};
export function CommissionCustomCard({
card,
}: {
card: CommissionCustomCardWithItems;
}) {
return (
<div className="flex flex-col h-full">
<div className="overflow-hidden h-full">
<Card
className={cn(
"flex flex-col h-full relative shadow-sm",
card.isSpecialOffer
? "border-2 border-primary/50"
: "border-border",
)}
>
{card.isSpecialOffer ? (
<div className="pointer-events-none absolute right-0 top-0 z-10 overflow-visible">
<div className="absolute right-0 top-16 h-7 w-36 origin-top-right translate-x-10 rotate-45 bg-primary text-primary-foreground shadow-md">
<span className="flex h-full w-full items-center justify-center text-xs font-semibold uppercase tracking-wide">
Special
</span>
</div>
</div>
) : null}
<CardHeader className="gap-2">
<CardTitle className="text-xl font-bold">{card.name}</CardTitle>
<p className="text-muted-foreground text-sm">{card.description}</p>
{card.referenceImageUrl ? (
<Dialog>
<DialogTrigger asChild>
<button
type="button"
className="group relative overflow-hidden rounded-lg border border-border/60 bg-muted/40"
>
<Image
src={card.referenceImageUrl}
alt={`${card.name} reference`}
width={800}
height={600}
sizes="(max-width: 768px) 90vw, 400px"
className="h-auto w-full object-cover transition-transform duration-200 group-hover:scale-[1.02]"
/>
<span className="absolute inset-x-0 bottom-0 bg-background/70 px-2 py-1 text-xs text-foreground/80">
Click to enlarge
</span>
</button>
</DialogTrigger>
<DialogContent className="flex w-auto! max-w-[95vw]! flex-col p-4 sm:p-6">
<DialogHeader className="sr-only">
<DialogTitle>{card.name} reference</DialogTitle>
</DialogHeader>
<div className="flex max-h-[85vh] max-w-[85vw] items-center justify-center rounded-xl border-border/60 bg-muted p-2 shadow-2xl">
<Image
src={card.referenceImageUrl}
alt={`${card.name} reference`}
width={1600}
height={1200}
sizes="85vw"
className="h-auto max-h-[85vh] w-auto max-w-[85vw] rounded-lg object-contain"
/>
</div>
</DialogContent>
</Dialog>
) : null}
</CardHeader>
<CardContent className="flex flex-1 flex-col justify-start gap-4">
<div>
<h4 className="font-semibold">Options</h4>
<ul className="pl-4 list-disc">
{card.options.map((option) => (
<li key={option.id}>
{option.option?.name}:{" "}
{option.price && option.price !== 0
? `${option.price}`
: option.pricePercent
? `+${option.pricePercent}%`
: option.priceRange && option.priceRange !== "00"
? `${option.priceRange}`
: "Included"}
</li>
))}
</ul>
</div>
<div>
{card.extras.length > 0 ? (
<h4 className="font-semibold">Extras</h4>
) : null}
<ul className="pl-4 list-disc">
{card.extras.map((extra) => (
<li key={extra.id}>
{extra.extra?.name}:{" "}
{extra.price && extra.price !== 0
? `${extra.price}`
: extra.pricePercent
? `+${extra.pricePercent}%`
: extra.priceRange && extra.priceRange !== "00"
? `${extra.priceRange}`
: "Included"}
</li>
))}
</ul>
</div>
</CardContent>
{card.tags.length > 0 ? (
<div className="mt-auto px-6 pb-6">
<Link
href={`/portfolio/tagged?tags=${encodeURIComponent(
card.tags.map((t) => t.slug).join(","),
)}`}
>
<Button variant="secondary" className="w-full">
View example artworks
</Button>
</Link>
</div>
) : null}
</Card>
</div>
</div>
);
}

View File

@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -12,7 +13,10 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import type {
CommissionCustomCard,
CommissionCustomCardExtra,
CommissionCustomCardOption,
CommissionCustomInput, CommissionCustomInput,
CommissionExtra, CommissionExtra,
CommissionOption, CommissionOption,
@ -28,7 +32,7 @@ import Link from "next/link";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useForm, useWatch } from "react-hook-form"; import { useForm, useWatch } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import * as z from "zod/v4"; import type * as z from "zod/v4";
import { FileDropzone } from "./FileDropzone"; import { FileDropzone } from "./FileDropzone";
type CommissionTypeWithRelations = CommissionType & { type CommissionTypeWithRelations = CommissionType & {
@ -37,15 +41,40 @@ type CommissionTypeWithRelations = CommissionType & {
customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[]; customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[];
}; };
type Props = { type CommissionCustomCardWithRelations = CommissionCustomCard & {
types: CommissionTypeWithRelations[]; options: (CommissionCustomCardOption & { option: CommissionOption })[];
extras: (CommissionCustomCardExtra & { extra: CommissionExtra })[];
}; };
export function CommissionOrderForm({ types }: Props) { type Props = {
types: CommissionTypeWithRelations[];
customCards: CommissionCustomCardWithRelations[];
};
type SelectedOption = {
id: string;
optionId: string;
option: CommissionOption;
price: number | null;
pricePercent: number | null;
priceRange: string | null;
};
type SelectedExtra = {
id: string;
extraId: string;
extra: CommissionExtra;
price: number | null;
pricePercent: number | null;
priceRange: string | null;
};
export function CommissionOrderForm({ types, customCards }: Props) {
const form = useForm<z.infer<typeof commissionOrderSchema>>({ const form = useForm<z.infer<typeof commissionOrderSchema>>({
resolver: zodResolver(commissionOrderSchema), resolver: zodResolver(commissionOrderSchema),
defaultValues: { defaultValues: {
typeId: "", typeId: "",
customCardId: "",
optionId: "", optionId: "",
extraIds: [], extraIds: [],
customerName: "", customerName: "",
@ -59,19 +88,49 @@ export function CommissionOrderForm({ types }: Props) {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const typeId = useWatch({ control: form.control, name: "typeId" }); const typeId = useWatch({ control: form.control, name: "typeId" });
const customCardId = useWatch({ control: form.control, name: "customCardId" });
const optionId = useWatch({ control: form.control, name: "optionId" }); const optionId = useWatch({ control: form.control, name: "optionId" });
const extraIds = useWatch({ control: form.control, name: "extraIds" }); const extraIds = useWatch({ control: form.control, name: "extraIds" });
const selectedType = useMemo(() => types.find((t) => t.id === typeId), [types, typeId]); const selectedType = useMemo(() => types.find((t) => t.id === typeId), [types, typeId]);
const selectedCustomCard = useMemo(
() => customCards.find((c) => c.id === customCardId),
[customCards, customCardId]
);
const selection = useMemo<{
kind: "type" | "custom";
name: string;
options: SelectedOption[];
extras: SelectedExtra[];
} | null>(() => {
if (selectedCustomCard) {
return {
kind: "custom",
name: selectedCustomCard.name,
options: selectedCustomCard.options,
extras: selectedCustomCard.extras,
};
}
if (selectedType) {
return {
kind: "type",
name: selectedType.name,
options: selectedType.options,
extras: selectedType.extras,
};
}
return null;
}, [selectedCustomCard, selectedType]);
const selectedOption = useMemo( const selectedOption = useMemo(
() => selectedType?.options.find((o) => o.optionId === optionId), () => selection?.options.find((o) => o.optionId === optionId),
[selectedType, optionId] [selection, optionId]
); );
const selectedExtras = useMemo( const selectedExtras = useMemo(
() => selectedType?.extras.filter((e) => extraIds?.includes(e.extraId)) ?? [], () => selection?.extras.filter((e) => extraIds?.includes(e.extraId)) ?? [],
[selectedType, extraIds] [selection, extraIds]
); );
const [minPrice, maxPrice] = useMemo(() => { const [minPrice, maxPrice] = useMemo(() => {
@ -84,6 +143,7 @@ export function CommissionOrderForm({ types }: Props) {
try { try {
const payload = { const payload = {
typeId: values.typeId || null, typeId: values.typeId || null,
customCardId: values.customCardId || null,
optionId: values.optionId || null, optionId: values.optionId || null,
extraIds: values.extraIds ?? [], extraIds: values.extraIds ?? [],
customerName: values.customerName, customerName: values.customerName,
@ -100,6 +160,7 @@ export function CommissionOrderForm({ types }: Props) {
form.reset({ form.reset({
typeId: "", typeId: "",
customCardId: "",
optionId: "", optionId: "",
extraIds: [], extraIds: [],
customerName: "", customerName: "",
@ -136,7 +197,12 @@ export function CommissionOrderForm({ types }: Props) {
key={type.id} key={type.id}
type="button" type="button"
variant={field.value === type.id ? "default" : "outline"} variant={field.value === type.id ? "default" : "outline"}
onClick={() => field.onChange(type.id)} onClick={() => {
field.onChange(type.id);
form.setValue("customCardId", "");
form.setValue("optionId", "");
form.setValue("extraIds", []);
}}
disabled={isSubmitting} disabled={isSubmitting}
> >
{type.name} {type.name}
@ -149,7 +215,40 @@ export function CommissionOrderForm({ types }: Props) {
)} )}
/> />
{selectedType && ( {customCards.length > 0 ? (
<FormField
control={form.control}
name="customCardId"
render={({ field }) => (
<FormItem>
<FormLabel>Custom requests / YCH</FormLabel>
<FormControl>
<div className="flex flex-wrap gap-2">
{customCards.map((card) => (
<Button
key={card.id}
type="button"
variant={field.value === card.id ? "default" : "outline"}
onClick={() => {
field.onChange(card.id);
form.setValue("typeId", "");
form.setValue("optionId", "");
form.setValue("extraIds", []);
}}
disabled={isSubmitting}
>
{card.name}
</Button>
))}
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
) : null}
{selection && (
<> <>
<FormField <FormField
control={form.control} control={form.control}
@ -159,7 +258,7 @@ export function CommissionOrderForm({ types }: Props) {
<FormLabel>Base Option</FormLabel> <FormLabel>Base Option</FormLabel>
<FormControl> <FormControl>
<div className="space-y-1"> <div className="space-y-1">
{selectedType.options.map((opt) => ( {selection.options.map((opt) => (
<label key={opt.id} className="flex items-center gap-2"> <label key={opt.id} className="flex items-center gap-2">
<input <input
type="radio" type="radio"
@ -186,7 +285,7 @@ export function CommissionOrderForm({ types }: Props) {
<FormLabel>Extras</FormLabel> <FormLabel>Extras</FormLabel>
<FormControl> <FormControl>
<div className="space-y-1"> <div className="space-y-1">
{selectedType.extras.map((ext) => ( {selection.extras.map((ext) => (
<label key={ext.id} className="flex items-center gap-2"> <label key={ext.id} className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
@ -221,8 +320,15 @@ export function CommissionOrderForm({ types }: Props) {
<FormItem> <FormItem>
<FormLabel>Your Name</FormLabel> <FormLabel>Your Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Nickname, real name, how you want to be called..." {...field} disabled={isSubmitting} /> <Input
placeholder="Nickname, real name, how you want to be called..."
{...field}
disabled={isSubmitting}
/>
</FormControl> </FormControl>
<FormDescription>
This name will be visible on the commission status page.
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -241,6 +347,9 @@ export function CommissionOrderForm({ types }: Props) {
disabled={isSubmitting} disabled={isSubmitting}
/> />
</FormControl> </FormControl>
<FormDescription>
Will be used for sending the invoice via paypal
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -259,6 +368,9 @@ export function CommissionOrderForm({ types }: Props) {
disabled={isSubmitting} disabled={isSubmitting}
/> />
</FormControl> </FormControl>
<FormDescription>
Optional. But if filled out, we need handle and which platform. Currently supported are fediverse, bsky and telegram
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@ -1,6 +1,9 @@
"use client"; "use client";
import NsfwBadge from "@/components/nsfw/NsfwBadge";
import NsfwConsentDialog from "@/components/nsfw/NsfwConsentDialog";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useNsfwStore } from "@/stores/nsfw-store";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { import {
@ -24,6 +27,9 @@ export type JustifiedGalleryItem = {
/** Optional: dominant color for hover ring. */ /** Optional: dominant color for hover ring. */
dominantHex?: string | null; dominantHex?: string | null;
/** Optional: NSFW flag */
nsfw?: boolean | null;
}; };
type Props = { type Props = {
@ -44,6 +50,10 @@ type Props = {
maxRowItems?: number; // desktop maxRowItems?: number; // desktop
maxRowItemsMobile?: number; // <640px maxRowItemsMobile?: number; // <640px
gap?: number; // px gap?: number; // px
gapNarrow?: number; // px for narrower containers
gapNarrowMaxWidth?: number; // px breakpoint for gapNarrow
gapBreakpoints?: Array<{ maxWidth: number; gap: number }>;
debug?: boolean;
className?: string; className?: string;
}; };
@ -84,11 +94,39 @@ export default function JustifiedGallery({
maxRowItems = 5, maxRowItems = 5,
maxRowItemsMobile = 3, maxRowItemsMobile = 3,
gap = 12, gap = 12,
gapNarrow,
gapNarrowMaxWidth = 720,
gapBreakpoints,
debug = false,
className, className,
}: Props) { }: Props) {
const consent = useNsfwStore((s) => s.consent);
const allowNsfw = consent === "allow";
const hasNsfw = useMemo(() => items.some((i) => i.nsfw), [items]);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const sentinelRef = useRef<HTMLDivElement | null>(null); const sentinelRef = useRef<HTMLDivElement | null>(null);
const [containerWidth, setContainerWidth] = useState(0); const [containerWidth, setContainerWidth] = useState(0);
const effectiveGap = (() => {
if (gapBreakpoints && containerWidth > 0) {
const sorted = [...gapBreakpoints].sort(
(a, b) => a.maxWidth - b.maxWidth,
);
for (const bp of sorted) {
if (containerWidth <= bp.maxWidth) return bp.gap;
}
}
if (
gapNarrow != null &&
containerWidth > 0 &&
containerWidth <= gapNarrowMaxWidth
) {
return gapNarrow;
}
return gap;
})();
// Measure container width (responsive) // Measure container width (responsive)
useEffect(() => { useEffect(() => {
@ -125,7 +163,12 @@ export default function JustifiedGallery({
const isMobile = containerWidth < 640; const isMobile = containerWidth < 640;
const targetH = isMobile ? targetRowHeightMobile : targetRowHeight; const targetH = isMobile ? targetRowHeightMobile : targetRowHeight;
const maxItems = isMobile ? maxRowItemsMobile : maxRowItems; const maxItems = (() => {
if (containerWidth < 480) return Math.min(2, maxRowItemsMobile);
if (containerWidth < 720) return Math.min(3, maxRowItems);
if (containerWidth < 1024) return Math.min(4, maxRowItems);
return maxRowItems;
})();
const rowTiles: RowTile[][] = []; const rowTiles: RowTile[][] = [];
let current: Array<{ item: JustifiedGalleryItem; aspect: number }> = []; let current: Array<{ item: JustifiedGalleryItem; aspect: number }> = [];
@ -136,7 +179,7 @@ export default function JustifiedGallery({
const flush = () => { const flush = () => {
if (current.length === 0) return; if (current.length === 0) return;
const gaps = gap * (current.length - 1); const gaps = effectiveGap * (current.length - 1);
const widthWithoutGaps = Math.max(0, available - gaps); const widthWithoutGaps = Math.max(0, available - gaps);
// Compute row height so it exactly fills the row width. // Compute row height so it exactly fills the row width.
@ -155,21 +198,64 @@ export default function JustifiedGallery({
aspectSum = 0; aspectSum = 0;
}; };
for (const it of items) { const workingItems = items.slice();
for (let i = 0; i < workingItems.length; i += 1) {
const it = workingItems[i];
const a = aspectOf(it); const a = aspectOf(it);
current.push({ item: it, aspect: a }); current.push({ item: it, aspect: a });
aspectSum += a; aspectSum += a;
// Estimate the row width if we were to keep targetH // Estimate the row width if we were to keep targetH
const estimatedWidth = aspectSum * targetH + gap * (current.length - 1); const estimatedWidth =
aspectSum * targetH + effectiveGap * (current.length - 1);
// If we've filled the row (or reached max items) and have at least 2 tiles, flush. // If we've filled the row (or reached max items) and have at least 2 tiles, flush.
if ( if (
(estimatedWidth >= available || current.length >= maxItems) && (estimatedWidth >= available || current.length >= maxItems) &&
current.length > 1 current.length > 1
) { ) {
flush(); const gaps = effectiveGap * (current.length - 1);
const widthWithoutGaps = Math.max(0, available - gaps);
const computedH = widthWithoutGaps / aspectSum;
// If the row would be shorter than maxRowHeight, reduce items and flush.
if (computedH < maxRowHeight && current.length > 1) {
const last = current.pop();
if (last) {
aspectSum -= last.aspect;
}
const limit = widthWithoutGaps / maxRowHeight;
let swapped = false;
for (let look = 1; look <= 2; look += 1) {
const idx = i + look;
if (idx >= workingItems.length) break;
const candidate = workingItems[idx];
const candidateAspect = aspectOf(candidate);
if (aspectSum + candidateAspect <= limit) {
workingItems[idx] = last?.item ?? candidate;
current.push({ item: candidate, aspect: candidateAspect });
aspectSum += candidateAspect;
swapped = true;
break;
}
}
flush();
if (!swapped && last) {
current = [last];
aspectSum = last.aspect;
} else {
current = [];
aspectSum = 0;
}
} else {
flush();
}
} }
} }
@ -178,7 +264,7 @@ export default function JustifiedGallery({
}, [ }, [
items, items,
containerWidth, containerWidth,
gap, effectiveGap,
targetRowHeight, targetRowHeight,
targetRowHeightMobile, targetRowHeightMobile,
maxRowHeight, maxRowHeight,
@ -192,27 +278,53 @@ export default function JustifiedGallery({
return `${first}-${last}-${row.length}`; return `${first}-${last}-${row.length}`;
}, []); }, []);
const isSmallScreen = containerWidth < 640;
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className={cn("mx-auto w-full max-w-6xl", className)} className={cn("mx-auto w-full max-w-6xl", className)}
> >
<NsfwConsentDialog hasNsfw={hasNsfw} />
<div className="space-y-3"> <div className="space-y-3">
{rows.map((row) => ( {rows.map((row, idx) => (
<div <div key={getRowKey(row)}>
key={getRowKey(row)} <div
className="flex justify-center" className={cn(
style={{ gap }} "flex",
> row.length === 1 && (isSmallScreen || idx !== rows.length - 1)
{row.map((t) => ( ? "justify-center"
<GalleryTile : idx === rows.length - 1
key={t.item.id} ? "justify-start"
tile={t} : "justify-between",
hrefBase={hrefBase} )}
hrefFrom={hrefFrom} style={{ columnGap: effectiveGap }}
showCaption={showCaption} >
/> {row.map((t) => (
))} <GalleryTile
key={t.item.id}
tile={t}
hrefBase={hrefBase}
hrefFrom={hrefFrom}
showCaption={showCaption}
allowNsfw={allowNsfw}
/>
))}
</div>
{debug ? (
<div className="text-xs text-muted-foreground font-mono">
{`row ${idx + 1} | h=${Math.round(row[0]?.h ?? 0)} | w=${Math.round(
row.reduce((sum, t) => sum + t.w, 0) +
effectiveGap * (row.length - 1),
)} | items=${row.length} | `}
{row
.map(
(t) =>
`${t.item.id}:${Math.round(t.w)}x${Math.round(t.h)} (src ${t.item.width}x${t.item.height})`,
)
.join(" | ")}
</div>
) : null}
</div> </div>
))} ))}
</div> </div>
@ -230,16 +342,20 @@ function GalleryTile({
hrefBase, hrefBase,
hrefFrom, hrefFrom,
showCaption, showCaption,
allowNsfw,
}: { }: {
tile: RowTile; tile: RowTile;
hrefBase: string; hrefBase: string;
hrefFrom: string; hrefFrom: string;
showCaption: boolean; showCaption: boolean;
allowNsfw: boolean;
}) { }) {
const { item, w, h } = tile; const { item, w, h } = tile;
const href = `${hrefBase}/${item.id}?from=${encodeURIComponent(hrefFrom)}`; const href = `${hrefBase}/${item.id}?from=${encodeURIComponent(hrefFrom)}`;
const src = `/api/image/gallery/${item.fileKey}.webp`; const src = `/api/image/gallery/${item.fileKey}.webp`;
const isNsfw = Boolean(item.nsfw);
const shouldBlur = isNsfw && !allowNsfw;
const style: CSSProperties & { "--dom"?: string } = {}; const style: CSSProperties & { "--dom"?: string } = {};
const dom = normalizeColor(item.dominantHex); const dom = normalizeColor(item.dominantHex);
@ -274,11 +390,26 @@ function GalleryTile({
alt={item.altText ?? item.name ?? "Artwork"} alt={item.altText ?? item.name ?? "Artwork"}
width={w} width={w}
height={h} height={h}
className="h-full w-full object-cover" className={cn(
"h-full w-full object-cover transition-[filter,transform] duration-200",
shouldBlur ? "blur-2xl scale-[1.02]" : "blur-0",
)}
// Tiles are thumbnail-ish; bias Next toward small resources. // Tiles are thumbnail-ish; bias Next toward small resources.
sizes="(max-width: 640px) 90vw, (max-width: 1024px) 50vw, 320px" sizes="(max-width: 640px) 90vw, (max-width: 1024px) 50vw, 320px"
/> />
{isNsfw ? (
shouldBlur ? null : (
<NsfwBadge className="absolute left-2 bottom-2 z-10" />
)
) : null}
{shouldBlur ? (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/45">
<NsfwBadge className="scale-110 shadow-md" />
</div>
) : null}
{showCaption ? ( {showCaption ? (
<div className="pointer-events-none absolute inset-x-0 top-0 bg-black/60 p-3"> <div className="pointer-events-none absolute inset-x-0 top-0 bg-black/60 p-3">
<div className="text-sm font-medium text-white line-clamp-1"> <div className="text-sm font-medium text-white line-clamp-1">

View File

@ -1,11 +1,8 @@
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
// import { Fraunces } from "next/font/google";
import localFont from 'next/font/local'; import localFont from 'next/font/local';
import Image from "next/image"; import Image from "next/image";
// const pacifico = Fraunces({ weight: "700", subsets: ["latin"] });
const myFont = localFont({ const myFont = localFont({
src: './Echotopia-Regular.woff2', src: './Echotopia-Regular.woff2',
}) })

View File

@ -1,5 +1,32 @@
import pkg from "../../../package.json";
const appVersion =
process.env.NEXT_PUBLIC_APP_VERSION ??
pkg.version;
const gitSha =
process.env.NEXT_PUBLIC_GIT_SHA ??
process.env.VERCEL_GIT_COMMIT_SHA ??
process.env.GIT_COMMIT_SHA ??
"";
const deployEnv =
process.env.NEXT_PUBLIC_DEPLOY_ENV ??
process.env.NODE_ENV ??
"unknown";
const versionLabel = gitSha
? `v${appVersion}+${gitSha.slice(0, 7)}`
: `v${appVersion}`;
const envLabel = deployEnv === "production" ? "prod" : deployEnv;
const currentYear = new Date().getFullYear();
const copyrightYear =
currentYear > 2025
? `2025${currentYear}`
: "2025";
export default function Footer() { export default function Footer() {
return ( return (
<div>© 2026 by gaertan.art | All rights reserved</div> <div className="flex items-center justify-between text-xs text-muted-foreground">
<span>© {copyrightYear} by gaertan.art | All rights reserved</span>
<span>{versionLabel} · {envLabel}</span>
</div>
); );
} }

View File

@ -1,4 +1,5 @@
import ModeToggle from "./ModeToggle"; import ModeToggle from "./ModeToggle";
import NsfwModeToggle from "./NsfwModeToggle";
import TopNav from "./TopNav"; import TopNav from "./TopNav";
export default function Header() { export default function Header() {
@ -7,7 +8,10 @@ export default function Header() {
<div className="w-full"> <div className="w-full">
<div className="flex items-center justify-between px-4 md:px-8 py-2"> <div className="flex items-center justify-between px-4 md:px-8 py-2">
<TopNav /> <TopNav />
<ModeToggle /> <div className="flex items-center gap-2">
<NsfwModeToggle />
<ModeToggle />
</div>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,42 @@
"use client";
import { Eye, EyeOff } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useNsfwStore } from "@/stores/nsfw-store";
export default function NsfwModeToggle() {
const consent = useNsfwStore((s) => s.consent);
const setConsent = useNsfwStore((s) => s.setConsent);
const enabled = consent === "allow";
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
{enabled ? (
<Eye className="h-[1.2rem] w-[1.2rem]" />
) : (
<EyeOff className="h-[1.2rem] w-[1.2rem]" />
)}
<span className="sr-only">Toggle NSFW mode</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setConsent("allow")}>
Allow NSFW Content
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setConsent("deny")}>
Hide NSFW Content
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -1,7 +1,7 @@
"use client" "use client"
import { ThemeProvider as NextThemesProvider } from "next-themes" import { ThemeProvider as NextThemesProvider } from "next-themes"
import * as React from "react" import type * as React from "react"
export function ThemeProvider({ export function ThemeProvider({
children, children,

View File

@ -12,6 +12,7 @@ const links = [
{ href: "/artworks", label: "Portfolio" }, { href: "/artworks", label: "Portfolio" },
{ href: "/artworks/animalstudies", label: "Animal Studies" }, { href: "/artworks/animalstudies", label: "Animal Studies" },
{ href: "/commissions", label: "Commissions" }, { href: "/commissions", label: "Commissions" },
{ href: "/commissions/status", label: "Commission Status" },
{ href: "/tos", label: "Terms of Service" }, { href: "/tos", label: "Terms of Service" },
// { href: "/portfolio/artfight", label: "Artfight" }, // { href: "/portfolio/artfight", label: "Artfight" },
// { href: "/portfolio/minis", label: "Miniatures" }, // { href: "/portfolio/minis", label: "Miniatures" },

View File

@ -0,0 +1,19 @@
"use client";
import { EyeOff } from "lucide-react";
import { cn } from "@/lib/utils";
export default function NsfwBadge({ className }: { className?: string }) {
return (
<div
className={cn(
"inline-flex items-center gap-1 rounded-full bg-black/70 px-2 py-1 text-xs font-semibold text-white shadow-sm",
className,
)}
>
<EyeOff className="h-3 w-3" />
NSFW
</div>
);
}

View File

@ -0,0 +1,75 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { EyeOff } from "lucide-react";
import { useEffect, useState } from "react";
import { useNsfwStore } from "@/stores/nsfw-store";
export default function NsfwConsentDialog({ hasNsfw }: { hasNsfw: boolean }) {
const consent = useNsfwStore((s) => s.consent);
const setConsent = useNsfwStore((s) => s.setConsent);
const [open, setOpen] = useState(false);
useEffect(() => {
if (!hasNsfw) {
setOpen(false);
return;
}
if (consent === "unset") setOpen(true);
else setOpen(false);
}, [hasNsfw, consent]);
if (!hasNsfw) return null;
return (
<Dialog
open={open}
onOpenChange={(next) => {
if (!next && consent === "unset") return;
setOpen(next);
}}
>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<EyeOff className="h-5 w-5 text-destructive" />
Sensitive Content
</DialogTitle>
<DialogDescription>
Sensitive artworks ahead. Confirm youre 18+ to view it. You can
change this anytime with the NSFW toggle in the header.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setConsent("deny");
setOpen(false);
}}
>
Keep blurred
</Button>
<Button
onClick={() => {
setConsent("allow");
setOpen(false);
}}
>
Im 18+ · Show
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,47 @@
"use client";
import Image, { type ImageProps } from "next/image";
import { cn } from "@/lib/utils";
import { useNsfwStore } from "@/stores/nsfw-store";
import NsfwBadge from "./NsfwBadge";
type NsfwImageProps = ImageProps & {
nsfw?: boolean;
wrapperClassName?: string;
};
export default function NsfwImage({
nsfw = false,
wrapperClassName,
className,
...props
}: NsfwImageProps) {
const consent = useNsfwStore((s) => s.consent);
const allowNsfw = consent === "allow";
const shouldBlur = nsfw && !allowNsfw;
return (
<div>
<Image
{...props}
className={cn(
"transition-[filter,transform] duration-200",
shouldBlur ? "blur-2xl scale-[1.02]" : "blur-0",
className,
)}
/>
{nsfw ? shouldBlur ? null : (
<NsfwBadge className="absolute left-2 bottom-2 z-10" />
) : null}
{shouldBlur ? (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/45">
<NsfwBadge className="scale-110 shadow-md" />
</div>
) : null}
</div>
);
}

View File

@ -0,0 +1,38 @@
"use client";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { useNsfwStore } from "@/stores/nsfw-store";
export default function NsfwLink({
href,
nsfw = false,
className,
children,
}: {
href: string;
nsfw?: boolean;
className?: string;
children: React.ReactNode;
}) {
const consent = useNsfwStore((s) => s.consent);
const allowNsfw = consent === "allow";
const blocked = nsfw && !allowNsfw;
if (blocked) {
return (
<div
className={cn("block cursor-not-allowed", className)}
aria-disabled="true"
>
{children}
</div>
);
}
return (
<Link href={href} className={cn("block", className)}>
{children}
</Link>
);
}

View File

@ -31,6 +31,12 @@ export default function PortfolioGallery({
const [items, setItems] = useState<PortfolioArtworkItem[]>([]); const [items, setItems] = useState<PortfolioArtworkItem[]>([]);
const [done, setDone] = useState(false); const [done, setDone] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const showScreenDebug = false;
const [screen, setScreen] = useState<{
width: number;
height: number;
dpr: number;
} | null>(null);
const inFlight = useRef(false); const inFlight = useRef(false);
const doneRef = useRef(false); const doneRef = useRef(false);
@ -80,27 +86,48 @@ export default function PortfolioGallery({
void loadMore(); void loadMore();
}, [loadMore]); }, [loadMore]);
useEffect(() => {
if (!showScreenDebug) return;
const update = () => {
setScreen({
width: window.innerWidth,
height: window.innerHeight,
dpr: window.devicePixelRatio || 1,
});
};
update();
window.addEventListener("resize", update);
window.addEventListener("orientationchange", update);
return () => {
window.removeEventListener("resize", update);
window.removeEventListener("orientationchange", update);
};
}, []);
const galleryItems: JustifiedGalleryItem[] = items.map((it) => ({ const galleryItems: JustifiedGalleryItem[] = items.map((it) => ({
id: it.id, id: it.id,
name: it.name, name: it.name,
altText: it.altText, altText: it.altText,
nsfw: it.nsfw,
fileKey: it.fileKey, fileKey: it.fileKey,
width: it.thumbW, width: it.thumbW,
height: it.thumbH, height: it.thumbH,
dominantHex: it.dominantHex, dominantHex: it.dominantHex,
})); }));
useEffect(() => { // useEffect(() => {
if (items.length === 0) return; // if (items.length === 0) return;
// Debug: inspect dominantHex values coming from the server. // // Debug: inspect dominantHex values coming from the server.
console.log( // console.log(
"[PortfolioGallery] dominantHex sample", // "[PortfolioGallery] dominantHex sample",
items.slice(0, 5).map((it) => ({ // items.slice(0, 5).map((it) => ({
id: it.id, // id: it.id,
dominantHex: it.dominantHex, // dominantHex: it.dominantHex,
})) // }))
); // );
}, [items]); // }, [items]);
if (!loading && done && galleryItems.length === 0) { if (!loading && done && galleryItems.length === 0) {
return ( return (
@ -111,17 +138,28 @@ export default function PortfolioGallery({
} }
return ( return (
<div className="w-full"> <div className="relative w-full">
{showScreenDebug && screen ? (
<div className="pointer-events-none absolute right-2 top-2 z-10 rounded border border-border/60 bg-background/80 px-2 py-1 text-[11px] font-mono text-muted-foreground shadow-sm">
Screen {screen.width} × {screen.height} · {screen.dpr}x
</div>
) : null}
<JustifiedGallery <JustifiedGallery
items={galleryItems} items={galleryItems}
hrefFrom="portfolio" hrefFrom="portfolio"
showCaption={false} showCaption={false}
debug={false}
targetRowHeight={160} targetRowHeight={160}
targetRowHeightMobile={160} targetRowHeightMobile={160}
maxRowHeight={300} maxRowHeight={300}
maxRowItems={5} maxRowItems={5}
maxRowItemsMobile={1} maxRowItemsMobile={1}
gap={12} gap={12}
gapBreakpoints={[
{ maxWidth: 685, gap: 6 },
{ maxWidth: 910, gap: 8 },
{ maxWidth: 1130, gap: 10 },
]}
onLoadMore={done ? undefined : () => void loadMore()} onLoadMore={done ? undefined : () => void loadMore()}
hasMore={!done} hasMore={!done}
isLoadingMore={loading} isLoadingMore={loading}

View File

@ -0,0 +1,118 @@
"use client";
import type {
Cursor,
TaggedArtworkItem,
} from "@/actions/portfolio/getTaggedArtworksPage";
import { getTaggedArtworksPage } from "@/actions/portfolio/getTaggedArtworksPage";
import JustifiedGallery, {
type JustifiedGalleryItem,
} from "@/components/gallery/JustifiedGallery";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
export default function TaggedGallery({ tagSlugs }: { tagSlugs: string[] }) {
const normalizedSlugs = useMemo(
() => tagSlugs.map((s) => s.trim()).filter(Boolean),
[tagSlugs],
);
const resetKey = useMemo(
() => normalizedSlugs.slice().sort().join(","),
[normalizedSlugs],
);
const [items, setItems] = useState<TaggedArtworkItem[]>([]);
const [done, setDone] = useState(false);
const [loading, setLoading] = useState(false);
const inFlight = useRef(false);
const doneRef = useRef(false);
doneRef.current = done;
const cursorRef = useRef<Cursor>(null);
useEffect(() => {
void resetKey;
setItems([]);
setDone(false);
doneRef.current = false;
inFlight.current = false;
cursorRef.current = null;
}, [resetKey]);
const loadMore = useCallback(async () => {
if (inFlight.current || doneRef.current || normalizedSlugs.length === 0)
return 0;
inFlight.current = true;
setLoading(true);
try {
const data = await getTaggedArtworksPage({
take: 60,
cursor: cursorRef.current,
tagSlugs: normalizedSlugs,
onlyPublished: true,
});
setItems((prev) => {
const seen = new Set(prev.map((x) => x.id));
const next = data.items.filter((x) => !seen.has(x.id));
return prev.concat(next);
});
cursorRef.current = data.nextCursor;
if (!data.nextCursor) setDone(true);
return data.items.length;
} finally {
setLoading(false);
inFlight.current = false;
}
}, [normalizedSlugs]);
useEffect(() => {
void loadMore();
}, [loadMore]);
const galleryItems: JustifiedGalleryItem[] = items.map((it) => ({
id: it.id,
name: it.name,
altText: it.altText,
nsfw: it.nsfw,
fileKey: it.fileKey,
width: it.thumbW,
height: it.thumbH,
dominantHex: it.dominantHex,
}));
if (!loading && done && galleryItems.length === 0) {
return (
<p className="text-muted-foreground text-center py-20">
No artworks to display
</p>
);
}
return (
<div className="w-full">
<JustifiedGallery
items={galleryItems}
hrefFrom="portfolio"
showCaption={false}
debug={false}
targetRowHeight={160}
targetRowHeightMobile={160}
maxRowHeight={300}
maxRowItems={5}
maxRowItemsMobile={1}
gap={12}
gapBreakpoints={[
{ maxWidth: 685, gap: 6 },
{ maxWidth: 910, gap: 8 },
{ maxWidth: 1130, gap: 10 },
]}
onLoadMore={done ? undefined : () => void loadMore()}
hasMore={!done}
isLoadingMore={loading}
/>
</div>
);
}

View File

@ -1,8 +1,8 @@
"use client" "use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion" import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react" import { ChevronDownIcon } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -63,4 +63,5 @@ function AccordionContent({
) )
} }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }

View File

@ -13,7 +13,7 @@ const buttonVariants = cva(
destructive: destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input dark:border-input dark:hover:bg-input/60",
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: ghost:

View File

@ -14,7 +14,7 @@ function Checkbox({
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
data-slot="checkbox" data-slot="checkbox"
className={cn( className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", "peer border-input dark:bg-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
{...props} {...props}

View File

@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className className

View File

@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea <textarea
data-slot="textarea" data-slot="textarea"
className={cn( className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className className
)} )}
{...props} {...props}

View File

@ -1,7 +1,8 @@
import * as z from "zod/v4" import * as z from "zod/v4"
export const commissionOrderSchema = z.object({ export const commissionOrderSchema = z.object({
typeId: z.string().min(1, "Please select a type"), typeId: z.string().optional(),
customCardId: z.string().optional(),
optionId: z.string().min(1, "Please choose a base option"), optionId: z.string().min(1, "Please choose a base option"),
extraIds: z.array(z.string()).optional(), extraIds: z.array(z.string()).optional(),
customFields: z.record(z.string(), z.unknown()).optional(), customFields: z.record(z.string(), z.unknown()).optional(),
@ -9,4 +10,23 @@ export const commissionOrderSchema = z.object({
customerEmail: z.email("Invalid email"), customerEmail: z.email("Invalid email"),
customerSocials: z.string().optional(), customerSocials: z.string().optional(),
message: z.string().min(5, "Please describe what you want"), message: z.string().min(5, "Please describe what you want"),
}).superRefine((data, ctx) => {
const hasType = Boolean(data.typeId && data.typeId.length > 0);
const hasCustom = Boolean(data.customCardId && data.customCardId.length > 0);
if (!hasType && !hasCustom) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["typeId"],
message: "Please select a commission type or a custom card",
});
}
if (hasType && hasCustom) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["typeId"],
message: "Choose either a type or a custom card, not both",
});
}
}) })

30
src/stores/nsfw-store.ts Normal file
View File

@ -0,0 +1,30 @@
"use client";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
export type NsfwConsent = "unset" | "allow" | "deny";
type NsfwStore = {
consent: NsfwConsent;
setConsent: (consent: NsfwConsent) => void;
allow: () => void;
deny: () => void;
reset: () => void;
};
export const useNsfwStore = create<NsfwStore>()(
persist(
(set) => ({
consent: "unset",
setConsent: (consent) => set({ consent }),
allow: () => set({ consent: "allow" }),
deny: () => set({ consent: "deny" }),
reset: () => set({ consent: "unset" }),
}),
{
name: "gaertan-nsfw-consent",
storage: createJSONStorage(() => localStorage),
},
),
);

View File

@ -10,7 +10,7 @@ export function calculatePrice(source: PriceSource, base: number): number {
if (source.priceRange) { if (source.priceRange) {
const parts = source.priceRange.split("").map(Number) const parts = source.priceRange.split("").map(Number)
const max = Math.max(...parts) const max = Math.max(...parts)
return isNaN(max) ? 0 : max return Number.isNaN(max) ? 0 : max
} }
return 0 return 0
} }
@ -39,8 +39,8 @@ export function calculatePriceRange(
const min = Number(minStr) const min = Number(minStr)
const max = Number(maxStr) const max = Number(maxStr)
if (!isNaN(min)) minExtra += min if (!Number.isNaN(min)) minExtra += min
if (!isNaN(max)) maxExtra += max if (!Number.isNaN(max)) maxExtra += max
} }
} }