Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c4107718d0
|
|||
|
1a855b2177
|
|||
|
9121b74ade
|
|||
|
90c27ff60a
|
|||
|
79b186889b
|
|||
|
1952eb89a3
|
|||
|
cee86edf44
|
|||
|
26118d2897
|
|||
|
d70f00314b
|
|||
|
874aa5f343
|
|||
|
b559b8250f
|
|||
|
d6695e4c1d
|
|||
|
145770afbe
|
|||
|
0d1dd3b0fe
|
|||
|
5f05557682
|
|||
|
ea354e5a9f
|
|||
|
940e934237
|
|||
|
aa95635e3e
|
|||
|
1940867519
|
|||
|
3e6d045cbd
|
|||
|
c712f31759
|
|||
|
eb8dcd54a8
|
|||
|
030065631c
|
|||
|
5a3e567ed5
|
|||
|
84dc219a14
|
|||
|
96efd4c942
|
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
156
bun.lock
156
bun.lock
@ -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.974.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.962.0",
|
"@aws-sdk/s3-request-presigner": "^3.974.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,36 @@
|
|||||||
"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.17.2",
|
||||||
"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.6.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",
|
||||||
},
|
},
|
||||||
"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 +72,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 +252,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 +380,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 +410,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 +436,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 +452,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 +526,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 +768,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 +786,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 +828,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 +870,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=="],
|
||||||
|
|
||||||
@ -960,11 +964,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 +1016,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
1815
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@ -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,14 +32,14 @@
|
|||||||
"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",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
@ -47,11 +49,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ model Artwork {
|
|||||||
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 {
|
||||||
@ -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())
|
||||||
@ -282,6 +335,7 @@ model CommissionOption {
|
|||||||
description String?
|
description String?
|
||||||
|
|
||||||
types CommissionTypeOption[]
|
types CommissionTypeOption[]
|
||||||
|
customCards CommissionCustomCardOption[]
|
||||||
requests CommissionRequest[]
|
requests CommissionRequest[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,6 +370,7 @@ model CommissionExtra {
|
|||||||
|
|
||||||
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())
|
||||||
@ -386,8 +479,10 @@ model CommissionRequest {
|
|||||||
|
|
||||||
optionId String?
|
optionId String?
|
||||||
typeId String?
|
typeId String?
|
||||||
|
customCardId String?
|
||||||
option CommissionOption? @relation(fields: [optionId], references: [id])
|
option CommissionOption? @relation(fields: [optionId], references: [id])
|
||||||
type CommissionType? @relation(fields: [typeId], references: [id])
|
type CommissionType? @relation(fields: [typeId], references: [id])
|
||||||
|
customCard CommissionCustomCard? @relation(fields: [customCardId], references: [id])
|
||||||
|
|
||||||
extras CommissionExtra[]
|
extras CommissionExtra[]
|
||||||
files CommissionRequestFile[]
|
files CommissionRequestFile[]
|
||||||
@ -399,6 +494,7 @@ model CommissionGuidelines {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
markdown String
|
markdown String
|
||||||
|
exampleImageUrl String?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
@@index([isActive])
|
@@index([isActive])
|
||||||
|
|||||||
94
src/actions/animalStudies/getAnimalStudiesPage.ts
Normal file
94
src/actions/animalStudies/getAnimalStudiesPage.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import type { JustifiedGalleryItem } from "@/components/gallery/JustifiedGallery";
|
||||||
|
import type { Prisma } from "@/generated/prisma/client";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export type AnimalStudiesCursor = { sortKey: number; id: string } | null;
|
||||||
|
|
||||||
|
export type AnimalStudiesPage = {
|
||||||
|
items: JustifiedGalleryItem[];
|
||||||
|
nextCursor: AnimalStudiesCursor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputSchema = z.object({
|
||||||
|
take: z.number().int().min(1).max(200).default(60),
|
||||||
|
cursor: z
|
||||||
|
.object({
|
||||||
|
sortKey: z.number().int(),
|
||||||
|
id: z.string().min(1),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
tagSlugs: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getAnimalStudiesPage(input: unknown): Promise<AnimalStudiesPage> {
|
||||||
|
const { take, cursor, tagSlugs } = inputSchema.parse(input);
|
||||||
|
|
||||||
|
const where: Prisma.ArtworkWhereInput = {
|
||||||
|
published: true,
|
||||||
|
// enforce deterministic ordering / pagination
|
||||||
|
sortKey: { not: null },
|
||||||
|
categories: { some: { name: "Animal Studies" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tagSlugs?.length) {
|
||||||
|
where.tags = { some: { slug: { in: tagSlugs } } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
where.OR = [
|
||||||
|
{ sortKey: { gt: cursor.sortKey } },
|
||||||
|
{ sortKey: cursor.sortKey, id: { gt: cursor.id } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await prisma.artwork.findMany({
|
||||||
|
where,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
altText: true,
|
||||||
|
sortKey: true,
|
||||||
|
file: { select: { fileKey: true } },
|
||||||
|
variants: {
|
||||||
|
where: { type: "resized" },
|
||||||
|
select: { width: true, height: true },
|
||||||
|
take: 1,
|
||||||
|
},
|
||||||
|
metadata: { select: { width: true, height: true } },
|
||||||
|
colors: {
|
||||||
|
select: { color: { select: { hex: true } } },
|
||||||
|
take: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ sortKey: "asc" }, { id: "asc" }],
|
||||||
|
take: take + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const slice = rows.slice(0, take);
|
||||||
|
const next = rows.length > take ? rows[take] : null;
|
||||||
|
|
||||||
|
const items: JustifiedGalleryItem[] = slice.map((r) => {
|
||||||
|
const v = r.variants[0];
|
||||||
|
const w = v?.width ?? r.metadata?.width ?? 4;
|
||||||
|
const h = v?.height ?? r.metadata?.height ?? 3;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
altText: r.altText,
|
||||||
|
fileKey: r.file.fileKey,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
dominantHex: r.colors?.[0]?.color?.hex ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextCursor: AnimalStudiesCursor =
|
||||||
|
next && next.sortKey != null ? { sortKey: next.sortKey, id: next.id } : null;
|
||||||
|
|
||||||
|
return { items, nextCursor };
|
||||||
|
}
|
||||||
@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"]) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { Prisma } from "@/generated/prisma/browser";
|
import type { Prisma } from "@/generated/prisma/browser";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
export type Cursor = {
|
export type Cursor = {
|
||||||
@ -11,7 +11,6 @@ export type Cursor = {
|
|||||||
export type PortfolioArtworkItem = {
|
export type PortfolioArtworkItem = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
|
||||||
altText: string | null;
|
altText: string | null;
|
||||||
|
|
||||||
sortKey: number | null;
|
sortKey: number | null;
|
||||||
@ -63,7 +62,8 @@ export async function getPortfolioArtworksPage(args: {
|
|||||||
|
|
||||||
const year = coerceYear(filters.year ?? null);
|
const year = coerceYear(filters.year ?? null);
|
||||||
const q = normQ(filters.q);
|
const q = normQ(filters.q);
|
||||||
const albumId = filters.albumId && filters.albumId !== "all" ? filters.albumId : null;
|
const albumId =
|
||||||
|
filters.albumId && filters.albumId !== "all" ? filters.albumId : null;
|
||||||
|
|
||||||
const baseWhere: Prisma.ArtworkWhereInput = {
|
const baseWhere: Prisma.ArtworkWhereInput = {
|
||||||
...(onlyPublished ? { published: true } : {}),
|
...(onlyPublished ? { published: true } : {}),
|
||||||
@ -79,10 +79,9 @@ export async function getPortfolioArtworksPage(args: {
|
|||||||
{
|
{
|
||||||
OR: [
|
OR: [
|
||||||
{ name: { contains: q, mode: "insensitive" } },
|
{ name: { contains: q, mode: "insensitive" } },
|
||||||
{ slug: { contains: q, mode: "insensitive" } },
|
{
|
||||||
{ altText: { contains: q, mode: "insensitive" } },
|
tags: { some: { name: { contains: q, mode: "insensitive" } } },
|
||||||
{ tags: { some: { name: { contains: q, mode: "insensitive" } } } },
|
},
|
||||||
{ albums: { some: { name: { contains: q, mode: "insensitive" } } } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -108,7 +107,6 @@ 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 = {
|
||||||
@ -130,14 +128,15 @@ export async function getPortfolioArtworksPage(args: {
|
|||||||
},
|
},
|
||||||
} satisfies Prisma.ArtworkSelect;
|
} satisfies Prisma.ArtworkSelect;
|
||||||
|
|
||||||
const mapRow = (r: any): PortfolioArtworkItem | null => {
|
type ArtworkRow = Prisma.ArtworkGetPayload<{ select: typeof select }>;
|
||||||
|
|
||||||
|
const mapRow = (r: ArtworkRow): PortfolioArtworkItem | null => {
|
||||||
const thumb = pickVariant(r.variants, "thumbnail");
|
const thumb = pickVariant(r.variants, "thumbnail");
|
||||||
if (!thumb?.width || !thumb?.height) return null;
|
if (!thumb?.width || !thumb?.height) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
name: r.name,
|
name: r.name,
|
||||||
slug: r.slug,
|
|
||||||
altText: r.altText ?? null,
|
altText: r.altText ?? null,
|
||||||
sortKey: r.sortKey ?? null,
|
sortKey: r.sortKey ?? null,
|
||||||
year: r.year ?? null,
|
year: r.year ?? null,
|
||||||
@ -171,20 +170,26 @@ export async function getPortfolioArtworksPage(args: {
|
|||||||
select,
|
select,
|
||||||
});
|
});
|
||||||
|
|
||||||
items = rowsA.map(mapRow).filter((x): x is PortfolioArtworkItem => x !== null);
|
items = rowsA
|
||||||
|
.map(mapRow)
|
||||||
|
.filter((x): x is PortfolioArtworkItem => x !== null);
|
||||||
|
|
||||||
if (items.length >= take) {
|
if (items.length >= take) {
|
||||||
const last = items[items.length - 1]!;
|
const last = items.at(-1);
|
||||||
nextCursor = { afterSortKey: last.sortKey!, afterId: last.id };
|
if (!last) {
|
||||||
|
return { items, nextCursor: null, total, years, albums };
|
||||||
|
}
|
||||||
|
if (last.sortKey == null) {
|
||||||
|
return { items, nextCursor: null, total, years, albums };
|
||||||
|
}
|
||||||
|
nextCursor = { afterSortKey: last.sortKey, afterId: last.id };
|
||||||
return { items, nextCursor, total, years, albums };
|
return { items, nextCursor, total, years, albums };
|
||||||
}
|
}
|
||||||
|
|
||||||
const remaining = take - items.length;
|
const remaining = take - items.length;
|
||||||
const lastAId = items.length ? items[items.length - 1]!.id : null;
|
|
||||||
|
|
||||||
const whereB: Prisma.ArtworkWhereInput = {
|
const whereB: Prisma.ArtworkWhereInput = {
|
||||||
AND: [where, { sortKey: null }],
|
AND: [where, { sortKey: null }],
|
||||||
...(lastAId ? { id: { gt: lastAId } } : {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowsB = await prisma.artwork.findMany({
|
const rowsB = await prisma.artwork.findMany({
|
||||||
@ -194,7 +199,9 @@ export async function getPortfolioArtworksPage(args: {
|
|||||||
select,
|
select,
|
||||||
});
|
});
|
||||||
|
|
||||||
const more = rowsB.map(mapRow).filter((x): x is PortfolioArtworkItem => x !== null);
|
const more = rowsB
|
||||||
|
.map(mapRow)
|
||||||
|
.filter((x): x is PortfolioArtworkItem => x !== null);
|
||||||
items = items.concat(more);
|
items = items.concat(more);
|
||||||
|
|
||||||
const last = items[items.length - 1];
|
const last = items[items.length - 1];
|
||||||
@ -218,11 +225,15 @@ export async function getPortfolioArtworksPage(args: {
|
|||||||
select,
|
select,
|
||||||
});
|
});
|
||||||
|
|
||||||
items = rowsB.map(mapRow).filter((x): x is PortfolioArtworkItem => x !== null);
|
items = rowsB
|
||||||
|
.map(mapRow)
|
||||||
|
.filter((x): x is PortfolioArtworkItem => x !== null);
|
||||||
|
|
||||||
const last = items[items.length - 1];
|
const last = items[items.length - 1];
|
||||||
nextCursor =
|
nextCursor =
|
||||||
items.length < take || !last ? null : { afterSortKey: null, afterId: last.id };
|
items.length < take || !last
|
||||||
|
? null
|
||||||
|
: { afterSortKey: null, afterId: last.id };
|
||||||
|
|
||||||
return { items, nextCursor, total, years, albums };
|
return { items, nextCursor, total, years, albums };
|
||||||
}
|
}
|
||||||
|
|||||||
173
src/actions/portfolio/getTaggedArtworksPage.ts
Normal file
173
src/actions/portfolio/getTaggedArtworksPage.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
"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;
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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 };
|
||||||
|
}
|
||||||
@ -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}`}
|
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>
|
||||||
|
<div>
|
||||||
<ArtworkList items={p.artworks} />
|
<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>
|
||||||
|
<div>
|
||||||
<ArtworkList items={p.artworks} />
|
<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} />
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import ArtworkThumbGallery from "@/components/artworks/ArtworkThumbGallery";
|
import AnimalStudiesGallery from "@/components/animalStudies/AnimalStudiesGallery";
|
||||||
import TagFilterDialog from "@/components/artworks/TagFilterDialog";
|
import TagFilterDialog from "@/components/artworks/TagFilterDialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
@ -17,68 +17,68 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function AnimalStudiesPage({ searchParams }: { searchParams: { tags?: string | string[] } }) {
|
export default async function AnimalStudiesPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: { tags?: string | string[] };
|
||||||
|
}) {
|
||||||
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);
|
||||||
|
|
||||||
const artworks = await prisma.artwork.findMany({
|
|
||||||
where: {
|
|
||||||
categories: { some: { name: "Animal Studies" } },
|
|
||||||
published: true,
|
|
||||||
...(expandedTagSlugs.length
|
|
||||||
? { tags: { some: { slug: { in: expandedTagSlugs } } } }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
file: true,
|
|
||||||
metadata: true,
|
|
||||||
tags: true,
|
|
||||||
variants: true,
|
|
||||||
colors: {
|
|
||||||
select: { color: { select: { hex: true } } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
orderBy: [{ sortKey: "asc" }, { id: "asc" }],
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log(JSON.stringify(artworks, null, 4))
|
|
||||||
|
|
||||||
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">
|
||||||
<header className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
<header className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||||
@ -88,16 +88,14 @@ export default async function AnimalStudiesPage({ searchParams }: { searchParams
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{selectedTagSlugs.length > 0
|
{selectedTagSlugs.length > 0
|
||||||
? `Filtered by ${selectedTagSlugs.length} tag${selectedTagSlugs.length === 1 ? "" : "s"}`
|
? `Filtered by ${selectedTagSlugs.length} tag${selectedTagSlugs.length === 1 ? "" : "s"
|
||||||
|
}`
|
||||||
: "Browse all published artworks in this category."}
|
: "Browse all published artworks in this category."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||||
<TagFilterDialog
|
<TagFilterDialog tags={tagsForFilter} selectedTagSlugs={selectedTagSlugs} />
|
||||||
tags={tagsForFilter}
|
|
||||||
selectedTagSlugs={selectedTagSlugs}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button asChild type="button" variant="secondary" className="h-11 gap-2">
|
<Button asChild type="button" variant="secondary" className="h-11 gap-2">
|
||||||
<Link href="/artworks/animalstudies/index">
|
<Link href="/artworks/animalstudies/index">
|
||||||
@ -108,7 +106,7 @@ export default async function AnimalStudiesPage({ searchParams }: { searchParams
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ArtworkThumbGallery items={artworks} fit={{ mode: "fixedWidth", width: 300 }} />
|
<AnimalStudiesGallery key={expandedTagSlugs.join(",")} tagSlugs={expandedTagSlugs} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { PortfolioFilters } from "@/actions/portfolio/getPortfolioArtworksPage";
|
import type { PortfolioFilters } from "@/actions/portfolio/getPortfolioArtworksPage";
|
||||||
import ColorMasonryGallery from "@/components/portfolio/ColorMasonryGallery";
|
|
||||||
import PortfolioFiltersBar from "@/components/portfolio/PortfolioFiltersBar";
|
import PortfolioFiltersBar from "@/components/portfolio/PortfolioFiltersBar";
|
||||||
|
import PortfolioGallery from "@/components/portfolio/PortfolioGallery";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
type SearchParams = {
|
type SearchParams = {
|
||||||
@ -14,11 +14,11 @@ function parseFilters(sp: SearchParams): PortfolioFilters {
|
|||||||
const yearRaw = sp.year?.trim();
|
const yearRaw = sp.year?.trim();
|
||||||
if (yearRaw && yearRaw !== "all") {
|
if (yearRaw && yearRaw !== "all") {
|
||||||
const y = Number(yearRaw);
|
const y = Number(yearRaw);
|
||||||
if (Number.isFinite(y) && y > 0) (filters as any).year = y;
|
if (Number.isFinite(y) && y > 0) filters.year = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
const qRaw = sp.q?.trim();
|
const qRaw = sp.q?.trim();
|
||||||
if (qRaw) (filters as any).q = qRaw;
|
if (qRaw) filters.q = qRaw;
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
@ -53,12 +53,22 @@ export default async function PortfolioPage({
|
|||||||
|
|
||||||
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="mb-6">
|
<header className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||||
<h1 className="text-2xl font-semibold">Portfolio</h1>
|
<div className="space-y-1">
|
||||||
<PortfolioFiltersBar years={years} />
|
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||||
|
Portfolio
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Browse all published artworks.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ColorMasonryGallery filters={filters} />
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||||
|
<PortfolioFiltersBar years={years} />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<PortfolioGallery filters={filters} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,12 @@ import { PlayCircle } from "lucide-react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
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,34 +29,44 @@ 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">
|
||||||
<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}`}>
|
<Link href={`/raw/${artwork.id}`}>
|
||||||
@ -94,6 +109,9 @@ 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>
|
||||||
);
|
);
|
||||||
|
|||||||
48
src/app/(normal)/artworks/tagged/page.tsx
Normal file
48
src/app/(normal)/artworks/tagged/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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([
|
||||||
|
prisma.commissionType.findMany({
|
||||||
include: {
|
include: {
|
||||||
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
|
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
|
||||||
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
|
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
|
||||||
customInputs: { include: { customInput: true }, orderBy: { sortIndex: "asc" } },
|
customInputs: {
|
||||||
|
include: { customInput: true },
|
||||||
|
orderBy: { sortIndex: "asc" },
|
||||||
|
},
|
||||||
|
tags: true,
|
||||||
},
|
},
|
||||||
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
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">
|
||||||
|
<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>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
125
src/app/(normal)/commissions/status/page.tsx
Normal file
125
src/app/(normal)/commissions/status/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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(0.12 0.035 255);
|
--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%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
71
src/app/rss.xml/route.ts
Normal 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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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[];
|
||||||
|
|||||||
76
src/components/animalStudies/AnimalStudiesGallery.tsx
Normal file
76
src/components/animalStudies/AnimalStudiesGallery.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import type { AnimalStudiesCursor } from "@/actions/animalStudies/getAnimalStudiesPage";
|
||||||
|
import { getAnimalStudiesPage } from "@/actions/animalStudies/getAnimalStudiesPage";
|
||||||
|
import JustifiedGallery, { type JustifiedGalleryItem } from "@/components/gallery/JustifiedGallery";
|
||||||
|
|
||||||
|
export default function AnimalStudiesGallery({
|
||||||
|
tagSlugs,
|
||||||
|
}: {
|
||||||
|
tagSlugs: string[];
|
||||||
|
}) {
|
||||||
|
const [items, setItems] = useState<JustifiedGalleryItem[]>([]);
|
||||||
|
const [cursor, setCursor] = useState<AnimalStudiesCursor>(null);
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const inFlight = useRef(false);
|
||||||
|
|
||||||
|
// Reset when tag filter changes (component key may already remount, but keep it safe)
|
||||||
|
useEffect(() => {
|
||||||
|
setItems([]);
|
||||||
|
setCursor(null);
|
||||||
|
setDone(false);
|
||||||
|
setLoading(false);
|
||||||
|
inFlight.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadMore = useCallback(async () => {
|
||||||
|
if (inFlight.current || done) return;
|
||||||
|
inFlight.current = true;
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getAnimalStudiesPage({
|
||||||
|
take: 60,
|
||||||
|
cursor,
|
||||||
|
tagSlugs,
|
||||||
|
});
|
||||||
|
|
||||||
|
setItems((prev) => {
|
||||||
|
const seen = new Set(prev.map((x) => x.id));
|
||||||
|
const next = res.items.filter((x) => !seen.has(x.id));
|
||||||
|
return prev.concat(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
setCursor(res.nextCursor);
|
||||||
|
if (!res.nextCursor) setDone(true);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
inFlight.current = false;
|
||||||
|
}
|
||||||
|
}, [cursor, done, tagSlugs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadMore();
|
||||||
|
}, [loadMore]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JustifiedGallery
|
||||||
|
items={items}
|
||||||
|
hrefFrom="animal-studies"
|
||||||
|
showCaption
|
||||||
|
targetRowHeight={160}
|
||||||
|
targetRowHeightMobile={160}
|
||||||
|
maxRowHeight={300}
|
||||||
|
maxRowItems={5}
|
||||||
|
maxRowItemsMobile={1}
|
||||||
|
gap={12}
|
||||||
|
onLoadMore={done ? undefined : () => void loadMore()}
|
||||||
|
hasMore={!done}
|
||||||
|
isLoadingMore={loading}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,117 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import React from "react";
|
|
||||||
import { ArtworkImageCard } from "./ArtworkImageCard";
|
|
||||||
|
|
||||||
type ArtworkGalleryItem = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
altText: string | null;
|
|
||||||
okLabL: number | null;
|
|
||||||
file: { fileKey: string };
|
|
||||||
metadata: { width: number; height: number } | null;
|
|
||||||
tags: { id: string; name: string }[];
|
|
||||||
colors: { color: { hex: string | null } }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type FitMode =
|
|
||||||
| { mode: "fixedWidth"; width: number } // height varies
|
|
||||||
| { mode: "fixedHeight"; height: number }; // width varies
|
|
||||||
|
|
||||||
function getOverlayTextClass(okLabL: number | null | undefined) {
|
|
||||||
return "text-white";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOverlayBgClass(okLabL: number | null | undefined) {
|
|
||||||
return "bg-black/45";
|
|
||||||
}
|
|
||||||
|
|
||||||
type OpenSheet = "alt" | "tags" | null;
|
|
||||||
|
|
||||||
const BUTTON_BAR_HEIGHT = 36;
|
|
||||||
|
|
||||||
export default function ArtworkThumbGallery({
|
|
||||||
items,
|
|
||||||
hrefBase = "/artworks",
|
|
||||||
fit = { mode: "fixedWidth", width: 400 },
|
|
||||||
}: {
|
|
||||||
items: ArtworkGalleryItem[];
|
|
||||||
hrefBase?: string;
|
|
||||||
fit?: FitMode;
|
|
||||||
}) {
|
|
||||||
const [openSheet, setOpenSheet] = React.useState<Record<string, OpenSheet>>({});
|
|
||||||
|
|
||||||
const toggleSheet = (id: string, which: Exclude<OpenSheet, null>) => {
|
|
||||||
setOpenSheet((prev) => {
|
|
||||||
const current = prev[id] ?? null;
|
|
||||||
// toggle off if same, switch if different
|
|
||||||
return { ...prev, [id]: current === which ? null : which };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
|
||||||
return <p className="text-muted-foreground italic">No artworks found.</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="grid gap-3.5 justify-center"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items.map((a) => {
|
|
||||||
const textClass = getOverlayTextClass(a.okLabL);
|
|
||||||
const bgClass = getOverlayBgClass(a.okLabL);
|
|
||||||
|
|
||||||
const w = a.metadata?.width ?? 4;
|
|
||||||
const h = a.metadata?.height ?? 3;
|
|
||||||
|
|
||||||
const tileStyle: React.CSSProperties =
|
|
||||||
fit.mode === "fixedWidth"
|
|
||||||
? { aspectRatio: `${w} / ${h}` }
|
|
||||||
: { height: fit.height, aspectRatio: `${w} / ${h}` };
|
|
||||||
|
|
||||||
const sheet = openSheet[a.id] ?? null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={a.id} className="w-full" style={tileStyle}>
|
|
||||||
<div className="relative h-full w-full">
|
|
||||||
<ArtworkImageCard
|
|
||||||
mode="tile"
|
|
||||||
href={`${hrefBase}/single/${a.id}?from=animal-studies`}
|
|
||||||
src={`/api/image/resized/${a.file.fileKey}.webp`}
|
|
||||||
alt={a.altText ?? a.name ?? "Artwork"}
|
|
||||||
width={a.metadata?.width ?? 0}
|
|
||||||
height={a.metadata?.height ?? 0}
|
|
||||||
aspectRatio={`${w} / ${h}`}
|
|
||||||
className="h-full w-full rounded-md"
|
|
||||||
imageClassName="object-cover"
|
|
||||||
style={{ ["--dom" as any]: a.colors[0]?.color?.hex ?? "#999999", }}
|
|
||||||
sizes="(min-width: 1280px) 20vw, (min-width: 1024px) 25vw, (min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Title overlay (restored) */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"pointer-events-none absolute left-0 right-0 top-0 px-3 py-2",
|
|
||||||
bgClass,
|
|
||||||
"backdrop-blur-[1px]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={cn("truncate text-sm font-medium", textClass)}>{a.name}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom reserved bar (if you need it later) */}
|
|
||||||
<div
|
|
||||||
className="absolute left-0 right-0 bottom-0 z-20 flex items-center justify-between px-2"
|
|
||||||
style={{ height: BUTTON_BAR_HEIGHT }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -7,7 +7,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import * as React from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
type Timelapse = {
|
type Timelapse = {
|
||||||
s3Key: string;
|
s3Key: string;
|
||||||
@ -25,11 +25,8 @@ export default function ArtworkTimelapseViewer({
|
|||||||
artworkName?: string | null;
|
artworkName?: string | null;
|
||||||
trigger: React.ReactNode;
|
trigger: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = React.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}
|
||||||
|
|||||||
@ -5,11 +5,12 @@ import Link from "next/link";
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
const FROM_TO_PATH: Record<string, string> = {
|
const FROM_TO_PATH: Record<string, string> = {
|
||||||
portfolio: "/portfolio",
|
portfolio: "/artworks",
|
||||||
"animal-studies": "/animal-studies",
|
"animal-studies": "/artworks/animalstudies",
|
||||||
|
"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") ?? "";
|
||||||
@ -18,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={[
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { FilterIcon, XIcon } from "lucide-react";
|
import { FilterIcon, XIcon } from "lucide-react";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import * as React from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -17,6 +17,7 @@ import {
|
|||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
|
||||||
type Tag = {
|
type Tag = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -24,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 }[];
|
||||||
};
|
};
|
||||||
@ -52,21 +52,19 @@ export default function TagFilterDialog({
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [draft, setDraft] = React.useState<string[]>(() => selectedTagSlugs);
|
const [draft, setDraft] = useState<string[]>(() => selectedTagSlugs);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
setDraft(selectedTagSlugs);
|
setDraft(selectedTagSlugs);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [selectedTagSlugs]);
|
||||||
}, [selectedTagSlugs.join(",")]);
|
|
||||||
|
|
||||||
const hasDraft = draft.length > 0;
|
const hasDraft = draft.length > 0;
|
||||||
const selectedSet = React.useMemo(() => new Set(draft), [draft]);
|
const selectedSet = useMemo(() => new Set(draft), [draft]);
|
||||||
|
|
||||||
const byId = React.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 = React.useMemo(() => {
|
|
||||||
const map = new Map<string, Tag[]>();
|
const map = new Map<string, Tag[]>();
|
||||||
for (const t of tags) {
|
for (const t of tags) {
|
||||||
if (!t.parentId) continue;
|
if (!t.parentId) continue;
|
||||||
@ -74,21 +72,20 @@ 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));
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [tags]);
|
}, [tags]);
|
||||||
|
|
||||||
const rootGroups = React.useMemo(() => {
|
const rootGroups = useMemo(() => {
|
||||||
return tags
|
return tags
|
||||||
.filter((t) => t.parentId === null)
|
.filter((t) => t.parentId === null)
|
||||||
.slice()
|
.slice()
|
||||||
.sort(sortTags);
|
.sort(sortTags);
|
||||||
}, [tags]);
|
}, [tags]);
|
||||||
|
|
||||||
const orphanChildren = React.useMemo(() => {
|
const orphanChildren = useMemo(() => {
|
||||||
return tags
|
return tags
|
||||||
.filter((t) => t.parentId !== null && !byId.has(t.parentId))
|
.filter((t) => t.parentId !== null && !byId.has(t.parentId))
|
||||||
.slice()
|
.slice()
|
||||||
@ -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);
|
||||||
@ -181,18 +177,15 @@ export default function TagFilterDialog({
|
|||||||
return (
|
return (
|
||||||
<div key={p.id} className="rounded-lg border p-4">
|
<div key={p.id} className="rounded-lg border p-4">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<label className="flex cursor-pointer items-center gap-3">
|
<Label className="flex cursor-pointer items-center gap-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={parentSelected}
|
checked={parentSelected}
|
||||||
onCheckedChange={(v) => onToggleParent(p, Boolean(v))}
|
onCheckedChange={(v) => onToggleParent(p, Boolean(v))}
|
||||||
/>
|
/>
|
||||||
<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>
|
||||||
|
|
||||||
<Badge variant={parentSelected ? "default" : "outline"}>
|
<Badge variant={parentSelected ? "default" : "outline"}>
|
||||||
{children.length} sub
|
{children.length} sub
|
||||||
@ -206,7 +199,7 @@ export default function TagFilterDialog({
|
|||||||
const disabled = parentSelected;
|
const disabled = parentSelected;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<Label
|
||||||
key={c.id}
|
key={c.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 rounded-md border px-3 py-2",
|
"flex items-center gap-3 rounded-md border px-3 py-2",
|
||||||
@ -220,7 +213,7 @@ export default function TagFilterDialog({
|
|||||||
onCheckedChange={(v) => onToggleChild(c.slug, Boolean(v))}
|
onCheckedChange={(v) => onToggleChild(c.slug, Boolean(v))}
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0 truncate text-sm">{c.name}</span>
|
<span className="min-w-0 truncate text-sm">{c.name}</span>
|
||||||
</label>
|
</Label>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@ -231,16 +224,11 @@ 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);
|
||||||
return (
|
return (
|
||||||
<label
|
<Label
|
||||||
key={t.id}
|
key={t.id}
|
||||||
className="flex cursor-pointer items-center gap-3 rounded-md border px-3 py-2 hover:bg-muted/50"
|
className="flex cursor-pointer items-center gap-3 rounded-md border px-3 py-2 hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
@ -249,7 +237,7 @@ export default function TagFilterDialog({
|
|||||||
onCheckedChange={(v) => onToggleChild(t.slug, Boolean(v))}
|
onCheckedChange={(v) => onToggleChild(t.slug, Boolean(v))}
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0 truncate text-sm">{t.name}</span>
|
<span className="min-w-0 truncate text-sm">{t.name}</span>
|
||||||
</label>
|
</Label>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
167
src/components/commissions/CommissionCustomCard.tsx
Normal file
167
src/components/commissions/CommissionCustomCard.tsx
Normal 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 !== "0–0"
|
||||||
|
? `${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 !== "0–0"
|
||||||
|
? `${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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export function FileDropzone({
|
|||||||
// Allow selecting the same file again later (if user removes and re-adds)
|
// Allow selecting the same file again later (if user removes and re-adds)
|
||||||
if (inputRef.current) inputRef.current.value = "";
|
if (inputRef.current) inputRef.current.value = "";
|
||||||
},
|
},
|
||||||
[append, files, maxFiles, onFilesSelected]
|
[append, files, maxFiles, onFilesSelected],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFiles = React.useCallback(
|
const handleFiles = React.useCallback(
|
||||||
@ -54,7 +54,7 @@ export function FileDropzone({
|
|||||||
if (incoming.length === 0) return;
|
if (incoming.length === 0) return;
|
||||||
mergeFiles(incoming);
|
mergeFiles(incoming);
|
||||||
},
|
},
|
||||||
[mergeFiles]
|
[mergeFiles],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
@ -75,6 +75,7 @@ export function FileDropzone({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// biome-ignore lint: lint/a11y/useSemanticElements
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@ -87,7 +88,7 @@ export function FileDropzone({
|
|||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full border-2 border-dashed rounded-md p-6 text-center cursor-pointer transition-colors",
|
"w-full border-2 border-dashed rounded-md p-6 text-center cursor-pointer transition-colors",
|
||||||
isDragging ? "border-primary bg-muted" : "border-muted-foreground/30"
|
isDragging ? "border-primary bg-muted" : "border-muted-foreground/30",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|||||||
389
src/components/gallery/JustifiedGallery.tsx
Normal file
389
src/components/gallery/JustifiedGallery.tsx
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
type CSSProperties,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export type JustifiedGalleryItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
altText: string | null;
|
||||||
|
fileKey: string;
|
||||||
|
|
||||||
|
/** Intrinsic dimensions of the resized/thumbnail variant */
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
|
||||||
|
/** Optional: dominant color for hover ring. */
|
||||||
|
dominantHex?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: JustifiedGalleryItem[];
|
||||||
|
hrefFrom: string;
|
||||||
|
hrefBase?: string; // default: "/artworks/single"
|
||||||
|
showCaption?: boolean;
|
||||||
|
|
||||||
|
// infinite scroll
|
||||||
|
onLoadMore?: () => void;
|
||||||
|
hasMore?: boolean;
|
||||||
|
isLoadingMore?: boolean;
|
||||||
|
|
||||||
|
// layout tuning
|
||||||
|
targetRowHeight?: number; // desktop
|
||||||
|
targetRowHeightMobile?: number; // <640px
|
||||||
|
maxRowHeight?: number;
|
||||||
|
maxRowItems?: number; // desktop
|
||||||
|
maxRowItemsMobile?: number; // <640px
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RowTile = {
|
||||||
|
item: JustifiedGalleryItem;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function aspectOf(it: JustifiedGalleryItem) {
|
||||||
|
const w = Math.max(1, it.width);
|
||||||
|
const h = Math.max(1, it.height);
|
||||||
|
return w / h;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeColor(value: string | null | undefined) {
|
||||||
|
if (!value) return null;
|
||||||
|
const v = value.trim();
|
||||||
|
if (!v) return null;
|
||||||
|
if (v.startsWith("#") || v.startsWith("rgb") || v.startsWith("hsl")) return v;
|
||||||
|
const hex = v.replace(/^0x/i, "");
|
||||||
|
if (/^[0-9a-fA-F]{3}$/.test(hex) || /^[0-9a-fA-F]{6}$/.test(hex)) {
|
||||||
|
return `#${hex}`;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
export default function JustifiedGallery({
|
||||||
|
items,
|
||||||
|
hrefFrom,
|
||||||
|
hrefBase = "/artworks/single",
|
||||||
|
showCaption = false,
|
||||||
|
onLoadMore,
|
||||||
|
hasMore = false,
|
||||||
|
isLoadingMore = false,
|
||||||
|
targetRowHeight = 220,
|
||||||
|
targetRowHeightMobile = 160,
|
||||||
|
maxRowHeight = 260,
|
||||||
|
maxRowItems = 5,
|
||||||
|
maxRowItemsMobile = 3,
|
||||||
|
gap = 12,
|
||||||
|
gapNarrow,
|
||||||
|
gapNarrowMaxWidth = 720,
|
||||||
|
gapBreakpoints,
|
||||||
|
debug = false,
|
||||||
|
className,
|
||||||
|
}: Props) {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const sentinelRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
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)
|
||||||
|
useEffect(() => {
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(() => setContainerWidth(el.clientWidth));
|
||||||
|
ro.observe(el);
|
||||||
|
setContainerWidth(el.clientWidth);
|
||||||
|
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Infinite scroll sentinel
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onLoadMore || !hasMore) return;
|
||||||
|
|
||||||
|
const el = sentinelRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0]?.isIntersecting && !isLoadingMore) onLoadMore();
|
||||||
|
},
|
||||||
|
{ rootMargin: "900px 0px" },
|
||||||
|
);
|
||||||
|
|
||||||
|
io.observe(el);
|
||||||
|
return () => io.disconnect();
|
||||||
|
}, [onLoadMore, hasMore, isLoadingMore]);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
if (!containerWidth) return [] as RowTile[][];
|
||||||
|
|
||||||
|
const isMobile = containerWidth < 640;
|
||||||
|
const targetH = isMobile ? targetRowHeightMobile : targetRowHeight;
|
||||||
|
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[][] = [];
|
||||||
|
let current: Array<{ item: JustifiedGalleryItem; aspect: number }> = [];
|
||||||
|
let aspectSum = 0;
|
||||||
|
|
||||||
|
const available = containerWidth;
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (current.length === 0) return;
|
||||||
|
|
||||||
|
const gaps = effectiveGap * (current.length - 1);
|
||||||
|
const widthWithoutGaps = Math.max(0, available - gaps);
|
||||||
|
|
||||||
|
// Compute row height so it exactly fills the row width.
|
||||||
|
const computedH = widthWithoutGaps / aspectSum;
|
||||||
|
const h = Math.min(computedH, maxRowHeight);
|
||||||
|
|
||||||
|
rowTiles.push(
|
||||||
|
current.map((x) => ({
|
||||||
|
item: x.item,
|
||||||
|
h,
|
||||||
|
w: Math.round(x.aspect * h),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
current = [];
|
||||||
|
aspectSum = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const workingItems = items.slice();
|
||||||
|
|
||||||
|
for (let i = 0; i < workingItems.length; i += 1) {
|
||||||
|
const it = workingItems[i];
|
||||||
|
const a = aspectOf(it);
|
||||||
|
|
||||||
|
current.push({ item: it, aspect: a });
|
||||||
|
aspectSum += a;
|
||||||
|
|
||||||
|
// Estimate the row width if we were to keep targetH
|
||||||
|
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 (
|
||||||
|
(estimatedWidth >= available || current.length >= maxItems) &&
|
||||||
|
current.length > 1
|
||||||
|
) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush();
|
||||||
|
return rowTiles;
|
||||||
|
}, [
|
||||||
|
items,
|
||||||
|
containerWidth,
|
||||||
|
effectiveGap,
|
||||||
|
targetRowHeight,
|
||||||
|
targetRowHeightMobile,
|
||||||
|
maxRowHeight,
|
||||||
|
maxRowItems,
|
||||||
|
maxRowItemsMobile,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getRowKey = useCallback((row: RowTile[]) => {
|
||||||
|
const first = row[0]?.item.id ?? "row";
|
||||||
|
const last = row.at(-1)?.item.id ?? "row";
|
||||||
|
return `${first}-${last}-${row.length}`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isSmallScreen = containerWidth < 640;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={cn("mx-auto w-full max-w-6xl", className)}
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{rows.map((row, idx) => (
|
||||||
|
<div key={getRowKey(row)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex",
|
||||||
|
row.length === 1 && (isSmallScreen || idx !== rows.length - 1)
|
||||||
|
? "justify-center"
|
||||||
|
: idx === rows.length - 1
|
||||||
|
? "justify-start"
|
||||||
|
: "justify-between",
|
||||||
|
)}
|
||||||
|
style={{ columnGap: effectiveGap }}
|
||||||
|
>
|
||||||
|
{row.map((t) => (
|
||||||
|
<GalleryTile
|
||||||
|
key={t.item.id}
|
||||||
|
tile={t}
|
||||||
|
hrefBase={hrefBase}
|
||||||
|
hrefFrom={hrefFrom}
|
||||||
|
showCaption={showCaption}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{onLoadMore ? <div ref={sentinelRef} className="h-px w-full" /> : null}
|
||||||
|
{isLoadingMore ? (
|
||||||
|
<p className="mt-3 text-sm text-muted-foreground">Loading…</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GalleryTile({
|
||||||
|
tile,
|
||||||
|
hrefBase,
|
||||||
|
hrefFrom,
|
||||||
|
showCaption,
|
||||||
|
}: {
|
||||||
|
tile: RowTile;
|
||||||
|
hrefBase: string;
|
||||||
|
hrefFrom: string;
|
||||||
|
showCaption: boolean;
|
||||||
|
}) {
|
||||||
|
const { item, w, h } = tile;
|
||||||
|
|
||||||
|
const href = `${hrefBase}/${item.id}?from=${encodeURIComponent(hrefFrom)}`;
|
||||||
|
const src = `/api/image/gallery/${item.fileKey}.webp`;
|
||||||
|
|
||||||
|
const style: CSSProperties & { "--dom"?: string } = {};
|
||||||
|
const dom = normalizeColor(item.dominantHex);
|
||||||
|
if (dom) style["--dom"] = dom;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
style={{ width: w, height: h, ...style }}
|
||||||
|
className={cn(
|
||||||
|
"group relative overflow-hidden rounded-lg border bg-background",
|
||||||
|
"transition-shadow hover:shadow-lg",
|
||||||
|
// keep border visible even if theme border is subtle
|
||||||
|
"border-border",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Solid vibrant hover ring (no gradient), driven by --dom.
|
||||||
|
Using box-shadow is more reliable than border-color overrides. */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 pointer-events-none rounded-lg transition-[box-shadow,opacity] duration-150",
|
||||||
|
// default no ring
|
||||||
|
"shadow-none opacity-0",
|
||||||
|
// on hover show ring
|
||||||
|
"group-hover:shadow-[inset_0_0_0_2px_var(--dom)]",
|
||||||
|
"group-hover:opacity-100",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={item.altText ?? item.name ?? "Artwork"}
|
||||||
|
width={w}
|
||||||
|
height={h}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
// Tiles are thumbnail-ish; bias Next toward small resources.
|
||||||
|
sizes="(max-width: 640px) 90vw, (max-width: 1024px) 50vw, 320px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showCaption ? (
|
||||||
|
<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">
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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" },
|
||||||
|
|||||||
@ -1,224 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import type {
|
|
||||||
Cursor,
|
|
||||||
PortfolioArtworkItem,
|
|
||||||
PortfolioFilters,
|
|
||||||
} from "@/actions/portfolio/getPortfolioArtworksPage";
|
|
||||||
import { getPortfolioArtworksPage } from "@/actions/portfolio/getPortfolioArtworksPage";
|
|
||||||
import { ArtworkImageCard } from "../artworks/ArtworkImageCard";
|
|
||||||
|
|
||||||
type Placement = {
|
|
||||||
id: string;
|
|
||||||
top: number;
|
|
||||||
left: number;
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
dominantHex: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function computeCols(
|
|
||||||
containerW: number,
|
|
||||||
gap: number,
|
|
||||||
minColW: number,
|
|
||||||
maxCols: number
|
|
||||||
) {
|
|
||||||
const cols = Math.max(
|
|
||||||
1,
|
|
||||||
Math.min(maxCols, Math.floor((containerW + gap) / (minColW + gap)))
|
|
||||||
);
|
|
||||||
const colW = Math.floor((containerW - gap * (cols - 1)) / cols);
|
|
||||||
return { cols, colW: Math.max(1, colW) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function packStableMasonry(
|
|
||||||
items: PortfolioArtworkItem[],
|
|
||||||
containerW: number,
|
|
||||||
opts: { gap: number; minColW: number; maxCols: number }
|
|
||||||
): { placements: Placement[]; height: number } {
|
|
||||||
const { gap, minColW, maxCols } = opts;
|
|
||||||
if (containerW <= 0 || items.length === 0) return { placements: [], height: 0 };
|
|
||||||
|
|
||||||
const { cols, colW } = computeCols(containerW, gap, minColW, maxCols);
|
|
||||||
const colHeights = Array(cols).fill(0) as number[];
|
|
||||||
const placements: Placement[] = [];
|
|
||||||
|
|
||||||
for (const it of items) {
|
|
||||||
let cBest = 0;
|
|
||||||
for (let c = 1; c < cols; c++) if (colHeights[c] < colHeights[cBest]) cBest = c;
|
|
||||||
|
|
||||||
const ratio = it.thumbH / it.thumbW;
|
|
||||||
const h = Math.round(colW * ratio);
|
|
||||||
|
|
||||||
const top = colHeights[cBest];
|
|
||||||
const left = cBest * (colW + gap);
|
|
||||||
|
|
||||||
placements.push({
|
|
||||||
id: it.id,
|
|
||||||
top,
|
|
||||||
left,
|
|
||||||
w: colW,
|
|
||||||
h,
|
|
||||||
dominantHex: it.dominantHex,
|
|
||||||
});
|
|
||||||
|
|
||||||
colHeights[cBest] = top + h + gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
const height = Math.max(...colHeights) - gap;
|
|
||||||
return { placements, height: Math.max(0, height) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function thumbUrl(fileKey: string) {
|
|
||||||
return `/api/image/resized/${fileKey}.webp`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useResizeObserverWidth() {
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
|
||||||
const [w, setW] = React.useState(0);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el) return;
|
|
||||||
const ro = new ResizeObserver(([e]) => setW(Math.floor(e.contentRect.width)));
|
|
||||||
ro.observe(el);
|
|
||||||
return () => ro.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { ref, w };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ColorMasonryGallery({
|
|
||||||
filters,
|
|
||||||
}: {
|
|
||||||
filters: PortfolioFilters;
|
|
||||||
}) {
|
|
||||||
const { ref: containerRef, w: containerW } = useResizeObserverWidth();
|
|
||||||
|
|
||||||
const [items, setItems] = React.useState<PortfolioArtworkItem[]>([]);
|
|
||||||
const [done, setDone] = React.useState(false);
|
|
||||||
const [loading, setLoading] = React.useState(false);
|
|
||||||
|
|
||||||
const inFlight = React.useRef(false);
|
|
||||||
const doneRef = React.useRef(false);
|
|
||||||
doneRef.current = done;
|
|
||||||
|
|
||||||
const cursorRef = React.useRef<Cursor>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setItems([]);
|
|
||||||
setDone(false);
|
|
||||||
doneRef.current = false;
|
|
||||||
inFlight.current = false;
|
|
||||||
cursorRef.current = null;
|
|
||||||
}, [filters]);
|
|
||||||
|
|
||||||
const loadMore = React.useCallback(async () => {
|
|
||||||
if (inFlight.current || doneRef.current) return 0;
|
|
||||||
inFlight.current = true;
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await getPortfolioArtworksPage({
|
|
||||||
take: 60,
|
|
||||||
cursor: cursorRef.current,
|
|
||||||
filters,
|
|
||||||
onlyPublished: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Defensive dedupe: prevents accidental repeats from any future cursor edge case
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}, [filters]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
void loadMore();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [loadMore]);
|
|
||||||
|
|
||||||
const sentinelRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
React.useEffect(() => {
|
|
||||||
const sentinel = sentinelRef.current;
|
|
||||||
if (!sentinel) return;
|
|
||||||
|
|
||||||
const io = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
if (entries.some((e) => e.isIntersecting)) void loadMore();
|
|
||||||
},
|
|
||||||
{ rootMargin: "900px 0px", threshold: 0.01 }
|
|
||||||
);
|
|
||||||
|
|
||||||
io.observe(sentinel);
|
|
||||||
return () => io.disconnect();
|
|
||||||
}, [loadMore]);
|
|
||||||
|
|
||||||
const GAP = 14;
|
|
||||||
const MIN_COL_W = 260;
|
|
||||||
const MAX_COLS = 6;
|
|
||||||
|
|
||||||
const { placements, height } = React.useMemo(() => {
|
|
||||||
return packStableMasonry(items, containerW, {
|
|
||||||
gap: GAP,
|
|
||||||
minColW: MIN_COL_W,
|
|
||||||
maxCols: MAX_COLS,
|
|
||||||
});
|
|
||||||
}, [items, containerW]);
|
|
||||||
|
|
||||||
const itemsById = React.useMemo(() => new Map(items.map((it) => [it.id, it])), [items]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={containerRef} className="w-full">
|
|
||||||
<div className="relative w-full" style={{ height }}>
|
|
||||||
{placements.map((p) => {
|
|
||||||
const it = itemsById.get(p.id);
|
|
||||||
if (!it) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={p.id}
|
|
||||||
className="absolute"
|
|
||||||
style={{
|
|
||||||
transform: `translate(${p.left}px, ${p.top}px)`,
|
|
||||||
width: p.w,
|
|
||||||
height: p.h,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* <div style={{ ["--dom" as any]: p.dominantHex }} className="h-full w-full"> */}
|
|
||||||
<ArtworkImageCard
|
|
||||||
mode="tile"
|
|
||||||
href={`/artworks/single/${it.id}?from=portfolio`}
|
|
||||||
src={thumbUrl(it.fileKey)}
|
|
||||||
alt={it.altText ?? it.name}
|
|
||||||
width={Math.max(1, it.thumbW)}
|
|
||||||
height={Math.max(1, it.thumbH)}
|
|
||||||
style={{ ["--dom" as any]: p.dominantHex }}
|
|
||||||
className="w-full h-full rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
// </div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!done && <div ref={sentinelRef} style={{ height: 1 }} />}
|
|
||||||
{loading && <p className="text-sm text-muted-foreground mt-3">Loading…</p>}
|
|
||||||
{!loading && done && items.length === 0 && (
|
|
||||||
<p className="text-muted-foreground text-center py-20">No artworks to display</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,7 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { FilterIcon, XIcon } from "lucide-react";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import * as React from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
function setParam(params: URLSearchParams, key: string, value?: string | null) {
|
function setParam(params: URLSearchParams, key: string, value?: string | null) {
|
||||||
if (!value) params.delete(key);
|
if (!value) params.delete(key);
|
||||||
@ -16,118 +31,119 @@ export default function PortfolioFiltersBar({ years = [] }: { years?: number[] }
|
|||||||
const yearParam = sp.get("year") ?? "all";
|
const yearParam = sp.get("year") ?? "all";
|
||||||
const qParam = sp.get("q") ?? "";
|
const qParam = sp.get("q") ?? "";
|
||||||
|
|
||||||
// Local input state (typing does NOT change URL)
|
const [open, setOpen] = useState(false);
|
||||||
const [q, setQ] = React.useState(qParam);
|
const [draftYear, setDraftYear] = useState<string>(yearParam);
|
||||||
|
const [draftQ, setDraftQ] = useState<string>(qParam);
|
||||||
|
|
||||||
// Sync input when navigating back/forward (URL -> input)
|
useEffect(() => {
|
||||||
React.useEffect(() => {
|
setDraftYear(yearParam);
|
||||||
setQ(qParam);
|
setDraftQ(qParam);
|
||||||
}, [qParam]);
|
}, [yearParam, qParam]);
|
||||||
|
|
||||||
const pushParams = React.useCallback(
|
const activeCount = (yearParam !== "all" ? 1 : 0) + (qParam.trim().length ? 1 : 0);
|
||||||
(mutate: (next: URLSearchParams) => void) => {
|
|
||||||
|
const clearAll = () => {
|
||||||
|
setDraftYear("all");
|
||||||
|
setDraftQ("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
const next = new URLSearchParams(sp.toString());
|
const next = new URLSearchParams(sp.toString());
|
||||||
mutate(next);
|
|
||||||
|
|
||||||
const nextQs = next.toString();
|
const year = draftYear.trim();
|
||||||
const currQs = sp.toString();
|
if (!year || year === "all") next.delete("year");
|
||||||
if (nextQs === currQs) return; // guard against redundant replaces
|
else setParam(next, "year", year);
|
||||||
|
|
||||||
router.replace(nextQs ? `${pathname}?${nextQs}` : pathname, { scroll: false });
|
const q = draftQ.trim();
|
||||||
},
|
if (!q) next.delete("q");
|
||||||
[pathname, router, sp]
|
else setParam(next, "q", q);
|
||||||
);
|
|
||||||
|
|
||||||
const setYear = (year: "all" | number) => {
|
const qs = next.toString();
|
||||||
pushParams((next) => {
|
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
|
||||||
setParam(next, "year", year === "all" ? null : String(year));
|
setOpen(false);
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitSearch = (value: string) => {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
pushParams((next) => {
|
|
||||||
setParam(next, "q", trimmed.length ? trimmed : null);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const clear = () => {
|
|
||||||
setQ("");
|
|
||||||
pushParams((next) => {
|
|
||||||
next.delete("year");
|
|
||||||
next.delete("q");
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 flex flex-col gap-3">
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<div className="flex flex-col gap-1">
|
<DialogTrigger asChild>
|
||||||
<div className="text-sm text-muted-foreground">Year</div>
|
<Button type="button" variant="default" className="h-11 gap-2">
|
||||||
<div className="flex flex-wrap gap-2">
|
<FilterIcon className="h-4 w-4" />
|
||||||
<button
|
Filter
|
||||||
type="button"
|
{activeCount > 0 ? (
|
||||||
onClick={() => setYear("all")}
|
<Badge variant="secondary" className="ml-1">
|
||||||
className={[
|
{activeCount}
|
||||||
"h-9 rounded-md border px-3 text-sm",
|
</Badge>
|
||||||
yearParam === "all" ? "bg-accent" : "hover:bg-accent/60",
|
) : null}
|
||||||
].join(" ")}
|
</Button>
|
||||||
>
|
</DialogTrigger>
|
||||||
All
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{years.map((y) => {
|
<DialogContent className="p-0 sm:max-w-xl">
|
||||||
const active = yearParam === String(y);
|
<DialogHeader className="px-6 pt-6">
|
||||||
return (
|
<DialogTitle className="flex items-center justify-between gap-3">
|
||||||
<button
|
<span>Filter portfolio</span>
|
||||||
key={y}
|
{draftYear !== "all" || draftQ.trim().length ? (
|
||||||
type="button"
|
<Button
|
||||||
onClick={() => setYear(y)}
|
variant="ghost"
|
||||||
className={[
|
size="sm"
|
||||||
"h-9 rounded-md border px-3 text-sm",
|
onClick={clearAll}
|
||||||
active ? "bg-accent" : "hover:bg-accent/60",
|
className="gap-2"
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
{y}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
className="flex flex-col gap-1 sm:max-w-xl"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
submitSearch(q);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="text-sm text-muted-foreground">Search (by name or tags)</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
className="h-10 w-full rounded-md border bg-background px-3 text-sm"
|
|
||||||
value={q}
|
|
||||||
onChange={(e) => setQ(e.target.value)}
|
|
||||||
placeholder="e.g. lizard, monk, fantasy"
|
|
||||||
inputMode="search"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="h-10 rounded-md border px-3 text-sm hover:bg-accent"
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="h-10 rounded-md border px-3 text-sm hover:bg-accent"
|
|
||||||
onClick={clear}
|
|
||||||
>
|
>
|
||||||
|
<XIcon className="h-4 w-4" />
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</DialogTitle>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Filter by year and search by artwork name or tags.
|
||||||
|
</p>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<ScrollArea className="max-h-[60vh] px-6 py-4">
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="portfolio-year">Year</Label>
|
||||||
|
<select
|
||||||
|
id="portfolio-year"
|
||||||
|
value={draftYear}
|
||||||
|
onChange={(e) => setDraftYear(e.target.value)}
|
||||||
|
className="h-11 w-full rounded-md border bg-background px-3 text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">All years</option>
|
||||||
|
{years.map((y) => (
|
||||||
|
<option key={y} value={String(y)}>
|
||||||
|
{y}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="portfolio-q">Search</Label>
|
||||||
|
<Input
|
||||||
|
id="portfolio-q"
|
||||||
|
value={draftQ}
|
||||||
|
onChange={(e) => setDraftQ(e.target.value)}
|
||||||
|
placeholder="Search name or tags"
|
||||||
|
inputMode="search"
|
||||||
|
className="h-11"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-2 px-6 py-4">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={apply}>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
168
src/components/portfolio/PortfolioGallery.tsx
Normal file
168
src/components/portfolio/PortfolioGallery.tsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Cursor,
|
||||||
|
PortfolioArtworkItem,
|
||||||
|
PortfolioFilters,
|
||||||
|
} from "@/actions/portfolio/getPortfolioArtworksPage";
|
||||||
|
import { getPortfolioArtworksPage } from "@/actions/portfolio/getPortfolioArtworksPage";
|
||||||
|
import JustifiedGallery, {
|
||||||
|
type JustifiedGalleryItem,
|
||||||
|
} from "@/components/gallery/JustifiedGallery";
|
||||||
|
|
||||||
|
export default function PortfolioGallery({
|
||||||
|
filters,
|
||||||
|
}: {
|
||||||
|
filters: PortfolioFilters;
|
||||||
|
}) {
|
||||||
|
const { year, albumId, q } = filters;
|
||||||
|
|
||||||
|
const queryFilters = useMemo<PortfolioFilters>(
|
||||||
|
() => ({ year, albumId, q }),
|
||||||
|
[year, albumId, q]
|
||||||
|
);
|
||||||
|
const resetKey = useMemo(
|
||||||
|
() => `${year ?? ""}|${albumId ?? ""}|${q ?? ""}`,
|
||||||
|
[year, albumId, q]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [items, setItems] = useState<PortfolioArtworkItem[]>([]);
|
||||||
|
const [done, setDone] = 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 doneRef = useRef(false);
|
||||||
|
doneRef.current = done;
|
||||||
|
const cursorRef = useRef<Cursor>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resetKey == null) return;
|
||||||
|
setItems([]);
|
||||||
|
setDone(false);
|
||||||
|
doneRef.current = false;
|
||||||
|
inFlight.current = false;
|
||||||
|
cursorRef.current = null;
|
||||||
|
}, [resetKey]);
|
||||||
|
|
||||||
|
const loadMore = useCallback(async () => {
|
||||||
|
if (inFlight.current || doneRef.current) return 0;
|
||||||
|
inFlight.current = true;
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getPortfolioArtworksPage({
|
||||||
|
take: 60,
|
||||||
|
cursor: cursorRef.current,
|
||||||
|
filters: queryFilters,
|
||||||
|
onlyPublished: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Defensive dedupe
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}, [queryFilters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void 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) => ({
|
||||||
|
id: it.id,
|
||||||
|
name: it.name,
|
||||||
|
altText: it.altText,
|
||||||
|
fileKey: it.fileKey,
|
||||||
|
width: it.thumbW,
|
||||||
|
height: it.thumbH,
|
||||||
|
dominantHex: it.dominantHex,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (items.length === 0) return;
|
||||||
|
// // Debug: inspect dominantHex values coming from the server.
|
||||||
|
// console.log(
|
||||||
|
// "[PortfolioGallery] dominantHex sample",
|
||||||
|
// items.slice(0, 5).map((it) => ({
|
||||||
|
// id: it.id,
|
||||||
|
// dominantHex: it.dominantHex,
|
||||||
|
// }))
|
||||||
|
// );
|
||||||
|
// }, [items]);
|
||||||
|
|
||||||
|
if (!loading && done && galleryItems.length === 0) {
|
||||||
|
return (
|
||||||
|
<p className="text-muted-foreground text-center py-20">
|
||||||
|
No artworks to display
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
src/components/portfolio/TaggedGallery.tsx
Normal file
116
src/components/portfolio/TaggedGallery.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
"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(() => {
|
||||||
|
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,
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 }
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -1,12 +1,32 @@
|
|||||||
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.any()).optional(),
|
customFields: z.record(z.string(), z.unknown()).optional(),
|
||||||
customerName: z.string().min(2, "Enter your name"),
|
customerName: z.string().min(2, "Enter your name"),
|
||||||
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user