25 Commits

Author SHA1 Message Date
ea5eb6fa59 Refactoring code 2026-02-03 10:13:41 +01:00
0c72a756c5 Changed some stuf on artworks table 2026-02-02 19:19:52 +01:00
c915df904d Add tags to commssion types and custom types. Add button for example images to cards 2026-02-02 17:00:03 +01:00
93a327c634 Add migration 2026-02-02 14:52:27 +01:00
3b8b3efa4a Merge dev into main 2026-02-02 14:44:48 +01:00
dd7cd6a944 Fix type error 2026-02-02 13:56:14 +01:00
ed81662ae5 Moving the arttags table to tags table part 2 2026-02-02 13:48:49 +01:00
7605ccb0aa Moving the arttags table to tags table part 1 2026-02-02 13:05:52 +01:00
6680ccc023 Fix dev Dockerfile 2026-02-02 00:52:39 +01:00
1b6763cc3a Fix footer copyright year 2026-02-01 22:50:18 +01:00
c2d2e6c5a7 Add versioning to footer 2026-02-01 22:38:08 +01:00
c42401383b Update packages 2026-02-01 21:59:19 +01:00
7e9c618267 Bump version to nextjs 16.1.6 2026-02-01 21:57:19 +01:00
752c93e460 Merge branch 'main' into dev 2026-02-01 19:35:07 +01:00
e83272d8e8 merged two files 2026-02-01 19:34:49 +01:00
90deab01b1 Fix artwork edit page 2026-02-01 19:32:09 +01:00
227db7e879 Add custom commission types 2026-02-01 16:27:54 +01:00
2015ea6f2e Add custom commission types 2026-02-01 16:21:20 +01:00
e869f19142 Add commission type example image 2026-01-31 16:37:24 +01:00
51cfde4d78 Fix docker file for prod 2026-01-31 10:48:37 +01:00
88bb301e84 Add new gallery variant 2026-01-31 01:34:13 +01:00
3ce4c9acfc Add timelapse upload and display to image edit page 2026-01-30 22:18:24 +01:00
95c44b7f8e Fix options 2026-01-29 16:24:09 +01:00
507e1b9ee4 Add extras and options CRUD, add sidebar, add kanban board, udpate packages 2026-01-29 16:18:57 +01:00
f9c14ed9fb Update packages 2026-01-23 15:50:15 +01:00
101 changed files with 15017 additions and 999 deletions

1
.gitignore vendored
View File

@ -41,3 +41,4 @@ yarn-error.log*
next-env.d.ts
/src/generated/prisma
/scripts/

View File

@ -18,6 +18,12 @@ RUN bunx prisma generate
# Uncomment the following line in case you want to disable telemetry during the build.
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
# Copy the rest of the code
RUN bun run build -d
@ -31,7 +37,13 @@ ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production \
PORT=3000 \
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 && \
useradd --system --uid 1001 --no-log-init -g nodejs nextjs

View File

@ -14,11 +14,16 @@ FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# RUN bunx prisma migrate deploy
RUN bunx prisma generate
# Uncomment the following line in case you want to disable telemetry during the build.
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
# Copy the rest of the code
RUN bun run build -d
@ -32,6 +37,12 @@ ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production \
PORT=3000 \
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 && \
useradd --system --uid 1001 --no-log-init -g nodejs nextjs
@ -45,3 +56,12 @@ USER nextjs
EXPOSE 3000
CMD ["bun", "./server.js"]
# One-off migrations image (run at deploy time with DATABASE_URL)
FROM base AS migrate
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY prisma ./prisma
COPY prisma.config.ts package.json ./
ENV NODE_ENV=production
CMD ["bunx", "prisma", "migrate", "deploy"]

356
bun.lock
View File

@ -5,26 +5,30 @@
"": {
"name": "admin.gaertan.art",
"dependencies": {
"@aws-sdk/client-s3": "^3.958.0",
"@aws-sdk/s3-request-presigner": "^3.958.0",
"@aws-sdk/client-s3": "^3.974.0",
"@aws-sdk/s3-request-presigner": "^3.974.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.2.2",
"@platejs/basic-nodes": "^52.0.11",
"@platejs/code-block": "^52.0.11",
"@platejs/indent": "^52.0.11",
"@platejs/list": "^52.0.11",
"@platejs/markdown": "^52.0.11",
"@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0",
"@platejs/markdown": "^52.1.0",
"@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.3.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
@ -35,7 +39,7 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"archiver": "^7.0.1",
"better-auth": "^1.4.10",
"better-auth": "^1.4.17",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@ -44,23 +48,23 @@
"dotenv": "^17.2.3",
"lowlight": "^3.3.0",
"lucide-react": "^0.561.0",
"next": "^16.1.1",
"next": "16.1.6",
"next-themes": "^0.4.6",
"node-vibrant": "^4.0.3",
"nodemailer": "^7.0.12",
"pg": "^8.16.3",
"platejs": "^52.0.15",
"react": "19.2.1",
"pg": "^8.17.2",
"platejs": "^52.0.17",
"react": "19.2.4",
"react-day-picker": "^9.13.0",
"react-dom": "19.2.1",
"react-hook-form": "^7.69.0",
"react-dom": "19.2.4",
"react-hook-form": "^7.71.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwind-scrollbar-hide": "^4.0.0",
"uuid": "^13.0.0",
"zod": "^4.3.4",
"zod": "^4.3.6",
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
@ -68,19 +72,23 @@
"@types/archiver": "^7.0.0",
"@types/culori": "^4.0.1",
"@types/date-fns": "^2.6.3",
"@types/node": "^20.19.27",
"@types/nodemailer": "^7.0.4",
"@types/node": "^20.19.30",
"@types/nodemailer": "^7.0.5",
"@types/pg": "^8.16.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react": "19.2.10",
"@types/react-dom": "19.2.3",
"@types/uuid": "^11.0.0",
"prisma": "^7.2.0",
"prisma": "^7.3.0",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
},
},
},
"overrides": {
"@types/react": "19.2.10",
"@types/react-dom": "19.2.3",
},
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@ -98,83 +106,81 @@
"@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.962.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.957.0", "@aws-sdk/credential-provider-node": "3.962.0", "@aws-sdk/middleware-bucket-endpoint": "3.957.0", "@aws-sdk/middleware-expect-continue": "3.957.0", "@aws-sdk/middleware-flexible-checksums": "3.957.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-location-constraint": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-sdk-s3": "3.957.0", "@aws-sdk/middleware-ssec": "3.957.0", "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/signature-v4-multi-region": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/eventstream-serde-browser": "^4.2.7", "@smithy/eventstream-serde-config-resolver": "^4.3.7", "@smithy/eventstream-serde-node": "^4.2.7", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-blob-browser": "^4.2.8", "@smithy/hash-node": "^4.2.7", "@smithy/hash-stream-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/md5-js": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@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.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-I2/1McBZCcM3PfM4ck8D6gnZR3K7+yl1fGkwTq/3ThEn9tdLjNwcdgTbPfxfX6LoecLrH9Ekoo+D9nmQ0T261w=="],
"@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-sesv2": ["@aws-sdk/client-sesv2@3.958.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/credential-provider-node": "3.958.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/signature-v4-multi-region": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@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.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-3x3n8IIxIMAkdpt9wy9zS7MO2lqTcJwQTdHMn6BlD7YUohb+r5Q4KCOEQ2uHWd4WIJv2tlbXnfypHaXReO/WXA=="],
"@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/client-sso": ["@aws-sdk/client-sso@3.958.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@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.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg=="],
"@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/core": ["@aws-sdk/core@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws-sdk/xml-builder": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw=="],
"@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.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-qSwSfI+qBU9HDsd6/4fM9faCxYJx2yDuHtj+NVOQ6XYDWQzFab/hUdwuKZ77Pi6goLF1pBZhJ2azaC2w7LbnTA=="],
"@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-env": ["@aws-sdk/credential-provider-env@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog=="],
"@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-http": ["@aws-sdk/credential-provider-http@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw=="],
"@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-ini": ["@aws-sdk/credential-provider-ini@3.962.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/credential-provider-env": "3.957.0", "@aws-sdk/credential-provider-http": "3.957.0", "@aws-sdk/credential-provider-login": "3.962.0", "@aws-sdk/credential-provider-process": "3.957.0", "@aws-sdk/credential-provider-sso": "3.958.0", "@aws-sdk/credential-provider-web-identity": "3.958.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-h0kVnXLW2d3nxbcrR/Pfg3W/+YoCguasWz7/3nYzVqmdKarGrpJzaFdoZtLgvDSZ8VgWUC4lWOTcsDMV0UNqUQ=="],
"@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-login": ["@aws-sdk/credential-provider-login@3.962.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-kHYH6Av2UifG3mPkpPUNRh/PuX6adaAcpmsclJdHdxlixMCRdh8GNeEihq480DC0GmfqdpoSf1w2CLmLLPIS6w=="],
"@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-node": ["@aws-sdk/credential-provider-node@3.962.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.957.0", "@aws-sdk/credential-provider-http": "3.957.0", "@aws-sdk/credential-provider-ini": "3.962.0", "@aws-sdk/credential-provider-process": "3.957.0", "@aws-sdk/credential-provider-sso": "3.958.0", "@aws-sdk/credential-provider-web-identity": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-CS78NsWRxLa+nWqeWBEYMZTLacMFIXs1C5WJuM9kD05LLiWL32ksljoPsvNN24Bc7rCSQIIMx/U3KGvkDVZMVg=="],
"@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-process": ["@aws-sdk/credential-provider-process@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg=="],
"@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-sso": ["@aws-sdk/credential-provider-sso@3.958.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.958.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/token-providers": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg=="],
"@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/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.958.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA=="],
"@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-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws-sdk/util-arn-parser": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iczcn/QRIBSpvsdAS/rbzmoBpleX1JBjXvCynMbDceVLBIcVrwT1hXECrhtIC2cjh4HaLo9ClAbiOiWuqt+6MA=="],
"@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-expect-continue": ["@aws-sdk/middleware-expect-continue@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-AlbK3OeVNwZZil0wlClgeI/ISlOt/SPUxBsIns876IFaVu/Pj3DgImnYhpcJuFRek4r4XM51xzIaGQXM6GDHGg=="],
"@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-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.957.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/crc64-nvme": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iJpeVR5V8se1hl2pt+k8bF/e9JO4KWgPCMjg8BtRspNtKIUGy7j6msYvbDixaKZaF2Veg9+HoYcOhwnZumjXSA=="],
"@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-host-header": ["@aws-sdk/middleware-host-header@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA=="],
"@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-location-constraint": ["@aws-sdk/middleware-location-constraint@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-y8/W7TOQpmDJg/fPYlqAhwA4+I15LrS7TwgUEoxogtkD8gfur9wFMRLT8LCyc9o4NMEcAnK50hSb4+wB0qv6tQ=="],
"@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-logger": ["@aws-sdk/middleware-logger@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ=="],
"@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-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA=="],
"@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-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-arn-parser": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-5B2qY2nR2LYpxoQP0xUum5A1UNvH2JQpLHDH1nWFNF/XetV7ipFHksMxPNhtJJ6ARaWhQIDXfOUj0jcnkJxXUg=="],
"@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-ssec": ["@aws-sdk/middleware-ssec@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-qwkmrK0lizdjNt5qxl4tHYfASh8DFpHXM1iDVo+qHe+zuslfMqQEGRkzxS8tJq/I+8F0c6v3IKOveKJAfIvfqQ=="],
"@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/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.957.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ=="],
"@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/nested-clients": ["@aws-sdk/nested-clients@3.958.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.957.0", "@aws-sdk/middleware-host-header": "3.957.0", "@aws-sdk/middleware-logger": "3.957.0", "@aws-sdk/middleware-recursion-detection": "3.957.0", "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/region-config-resolver": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-endpoints": "3.957.0", "@aws-sdk/util-user-agent-browser": "3.957.0", "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@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.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw=="],
"@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/region-config-resolver": ["@aws-sdk/region-config-resolver@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A=="],
"@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/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.962.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.957.0", "@aws-sdk/types": "3.957.0", "@aws-sdk/util-format-url": "3.957.0", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-tyxsGfLY4NSohLrJsFGXbE3j8jguWK+hdGaUQSD1gJPvmC0B82qOyJ7WBIJLWgTabU3fiF/I9EGXjzR2rKr8jQ=="],
"@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/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.957.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-t6UfP1xMUigMMzHcb7vaZcjv7dA2DQkk9C/OAP1dKyrE0vb4lFGDaTApi17GN6Km9zFxJthEMUbBc7DL0hq1Bg=="],
"@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/token-providers": ["@aws-sdk/token-providers@3.958.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q=="],
"@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/types": ["@aws-sdk/types@3.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg=="],
"@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-arn-parser": ["@aws-sdk/util-arn-parser@3.957.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Aj6m+AyrhWyg8YQ4LDPg2/gIfGHCEcoQdBt5DeSFogN5k9mmJPOJ+IAmNSWmWRjpOxEy6eY813RNDI6qS97M0g=="],
"@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-endpoints": ["@aws-sdk/util-endpoints@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-endpoints": "^3.2.7", "tslib": "^2.6.2" } }, "sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw=="],
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-Yyo/tlc0iGFGTPPkuxub1uRAv6XrnVnvSNjslZh5jIYA8GZoeEFPgJa3Qdu0GUS/YwoK8GOLnnaL9h/eH5LDJQ=="],
"@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-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.957.0", "", { "dependencies": { "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw=="],
"@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.957.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.957.0", "@aws-sdk/types": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q=="],
"@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.957.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA=="],
"@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=="],
"@better-auth/core": ["@better-auth/core@1.4.10", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg=="],
"@better-auth/core": ["@better-auth/core@1.4.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg=="],
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="],
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.18", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.18" } }, "sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ=="],
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
@ -212,15 +218,17 @@
"@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
"@dnd-kit/modifiers": ["@dnd-kit/modifiers@9.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw=="],
"@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="],
"@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
"@electric-sql/pglite": ["@electric-sql/pglite@0.3.2", "", {}, "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w=="],
"@electric-sql/pglite": ["@electric-sql/pglite@0.3.15", "", {}, "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ=="],
"@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.0.6", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.2" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw=="],
"@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.0.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg=="],
"@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.2.7", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.2" } }, "sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg=="],
"@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.2.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" } }, "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A=="],
"@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
@ -232,7 +240,7 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="],
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
"@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],
@ -320,25 +328,25 @@
"@juggle/resize-observer": ["@juggle/resize-observer@3.4.0", "", {}, "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="],
"@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.12.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w=="],
"@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.1", "", {}, "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA=="],
"@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA=="],
"@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.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw=="],
"@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.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ=="],
"@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.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg=="],
"@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.1", "", { "os": "linux", "cpu": "x64" }, "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ=="],
"@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.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA=="],
"@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.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA=="],
"@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.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="],
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
@ -350,43 +358,43 @@
"@platejs/code-block": ["@platejs/code-block@52.0.11", "", { "dependencies": { "react-compiler-runtime": "^1.0.0" }, "peerDependencies": { "platejs": ">=52.0.11", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-+MvKTvGhGQYNgEdExwnYXZZ58AmKYVOK6fIRX1JF8s6AHh8vUGuemOci8K65VhHSlMSChhVSVvOiAqkcgt3Z7w=="],
"@platejs/core": ["@platejs/core@52.0.15", "", { "dependencies": { "@platejs/slate": "52.0.10", "@udecode/react-hotkeys": "52.0.11", "@udecode/react-utils": "52.0.11", "@udecode/utils": "52.0.1", "clsx": "^2.1.1", "html-entities": "^2.6.0", "is-hotkey": "^0.2.0", "jotai": "~2.8.4", "jotai-optics": "0.4.0", "jotai-x": "2.3.3", "lodash": "^4.17.21", "nanoid": "^5.1.5", "optics-ts": "2.4.1", "react-compiler-runtime": "^1.0.0", "slate": "0.120.0", "slate-dom": "0.119.0", "slate-hyperscript": "0.100.0", "slate-react": "0.120.0", "use-deep-compare": "^1.3.0", "zustand": "^5.0.5", "zustand-x": "6.2.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-MoGsNuYMgHzStaqLdRjRiPMwGvWHnKGVBjmolSh2bXUhdzcko3ndm+kkZQrKBXN0QWDJKpFNwO5VgZmOXLfJuw=="],
"@platejs/core": ["@platejs/core@52.0.17", "", { "dependencies": { "@platejs/slate": "52.0.10", "@udecode/react-hotkeys": "52.0.11", "@udecode/react-utils": "52.0.11", "@udecode/utils": "52.0.1", "clsx": "^2.1.1", "html-entities": "^2.6.0", "is-hotkey": "^0.2.0", "jotai": "~2.8.4", "jotai-optics": "0.4.0", "jotai-x": "2.3.3", "lodash": "^4.17.21", "nanoid": "^5.1.5", "optics-ts": "2.4.1", "react-compiler-runtime": "^1.0.0", "slate": "0.120.0", "slate-dom": "0.119.0", "slate-hyperscript": "0.100.0", "slate-react": "0.120.0", "use-deep-compare": "^1.3.0", "zustand": "^5.0.5", "zustand-x": "6.2.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-AXwt/GfotU/+0WsCxVOI3tCR7svIX2eT+yGTCHriSDAPJYrRq6qxGUBzDJWV7M16Zh3ihMPHpE+sfOhnCfdYeg=="],
"@platejs/indent": ["@platejs/indent@52.0.11", "", { "dependencies": { "react-compiler-runtime": "^1.0.0" }, "peerDependencies": { "platejs": ">=52.0.11", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-CZYFOZObce4yzQal/coKtPzp2+N+DiLetLiYR2U8z4Tgc9da/Fsj11DNR7CorVcdNmBPWkr3vwbTwuSayNOYgQ=="],
"@platejs/list": ["@platejs/list@52.0.11", "", { "dependencies": { "@platejs/indent": "52.0.11", "clsx": "^2.1.1", "react-compiler-runtime": "^1.0.0" }, "peerDependencies": { "platejs": ">=52.0.11", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-azmJhBRQsjoWLwVWgTfHxegTg02k81sEJinwNv5gDPyiDlK1Pk98Oys/7H21SpwRWh75tPR/9JfV0ZGMqAHEJA=="],
"@platejs/markdown": ["@platejs/markdown@52.0.11", "", { "dependencies": { "marked": "^15.0.12", "mdast-util-math": "3.0.0", "mdast-util-mdx": "3.0.0", "react-compiler-runtime": "^1.0.0", "remark-mdx": "^3.1.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "ts-essentials": "10.1.0", "unified": "^11.0.5" }, "peerDependencies": { "platejs": ">=52.0.11", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-SsoFKC6LgMHsuVaJi/RQJ2ZjbB9B711RFgkVJYlGfdsqSrcw0bj2s421kRvDUhVMywfqEk2M5UyIgsefWgFFDw=="],
"@platejs/markdown": ["@platejs/markdown@52.1.0", "", { "dependencies": { "marked": "^15.0.12", "mdast-util-math": "3.0.0", "mdast-util-mdx": "3.0.0", "react-compiler-runtime": "^1.0.0", "remark-mdx": "^3.1.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "ts-essentials": "10.1.0", "unified": "^11.0.5" }, "peerDependencies": { "platejs": ">=52.0.11", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-4443H09CJ4AH1449TERm7ot5aFji4rKt+iHIZdB7FiIBz9Coki5+JkKcRn9gEq7lpjMQ9E/MD2J9ejnmglc1/A=="],
"@platejs/slate": ["@platejs/slate@52.0.10", "", { "dependencies": { "@udecode/utils": "52.0.1", "is-plain-object": "^5.0.0", "lodash": "^4.17.21", "slate": "0.120.0", "slate-dom": "0.119.0" } }, "sha512-15YwlCLkoWMFq02LCFBRRkYgv19iCGLiGCTGuWGwhhB2qZ4sWSklkQbJjFuwZIKG6k+EFPhzmAcJdNw3BrbSVw=="],
"@platejs/utils": ["@platejs/utils@52.0.15", "", { "dependencies": { "@platejs/core": "52.0.15", "@platejs/slate": "52.0.10", "@udecode/react-utils": "52.0.11", "@udecode/utils": "52.0.1", "clsx": "^2.1.1", "lodash": "^4.17.21", "react-compiler-runtime": "^1.0.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-qAu4E0ulLN3lmCkv0aFdgA/ogi6cAzkyYeXpMKQTOL3ixAyhjBSOPfWoz0bA99LfB20NZScKABE4qRPTuLRLvA=="],
"@platejs/utils": ["@platejs/utils@52.0.17", "", { "dependencies": { "@platejs/core": "52.0.17", "@platejs/slate": "52.0.10", "@udecode/react-utils": "52.0.11", "@udecode/utils": "52.0.1", "clsx": "^2.1.1", "lodash": "^4.17.21", "react-compiler-runtime": "^1.0.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-9YvYnic+qo4bPJAPvFwWQLeG5zQ/OWe6hyA7b3sFhmSyuiLWQZklkHrE96iyHu4Jn31h3FndbG9AH7e36HFa1w=="],
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.2.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.2.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-euIdQ13cRB2wZ3jPsnDnFhINquo1PYFPCg6yVL8b2rp3EdinQHsX9EDdCtRr489D5uhphcRk463OdQAFlsCr0w=="],
"@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/client": ["@prisma/client@7.2.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.2.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-JdLF8lWZ+LjKGKpBqyAlenxd/kXjd1Abf/xK+6vUA7R7L2Suo6AFTHFRpPSdAKCan9wzdFApsUpSa/F6+t1AtA=="],
"@prisma/client": ["@prisma/client@7.3.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.3.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-FXBIxirqQfdC6b6HnNgxGmU7ydCPEPk7maHMOduJJfnTP+MuOGa15X4omjR/zpPUUpm8ef/mEFQjJudOGkXFcQ=="],
"@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.2.0", "", {}, "sha512-dn7oB53v0tqkB0wBdMuTNFNPdEbfICEUe82Tn9FoKAhJCUkDH+fmyEp0ClciGh+9Hp2Tuu2K52kth2MTLstvmA=="],
"@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.3.0", "", {}, "sha512-dG/ceD9c+tnXATPk8G+USxxYM9E6UdMTnQeQ+1SZUDxTz7SgQcfxEqafqIQHcjdlcNK/pvmmLfSwAs3s2gYwUw=="],
"@prisma/config": ["@prisma/config@7.2.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-qmvSnfQ6l/srBW1S7RZGfjTQhc44Yl3ldvU6y3pgmuLM+83SBDs6UQVgMtQuMRe9J3gGqB0RF8wER6RlXEr6jQ=="],
"@prisma/config": ["@prisma/config@7.3.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-QyMV67+eXF7uMtKxTEeQqNu/Be7iH+3iDZOQZW5ttfbSwBamCSdwPszA0dum+Wx27I7anYTPLmRmMORKViSW1A=="],
"@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="],
"@prisma/debug": ["@prisma/debug@7.3.0", "", {}, "sha512-yh/tHhraCzYkffsI1/3a7SHX8tpgbJu1NPnuxS4rEpJdWAUDHUH25F1EDo6PPzirpyLNkgPPZdhojQK804BGtg=="],
"@prisma/dev": ["@prisma/dev@0.17.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.2", "@electric-sql/pglite-socket": "0.0.6", "@electric-sql/pglite-tools": "0.2.7", "@hono/node-server": "1.19.6", "@mrleebo/prisma-ast": "0.12.1", "@prisma/get-platform": "6.8.2", "@prisma/query-plan-executor": "6.18.0", "foreground-child": "3.3.1", "get-port-please": "3.1.2", "hono": "4.10.6", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.21.3", "std-env": "3.9.0", "valibot": "1.2.0", "zeptomatch": "2.0.2" } }, "sha512-6sGebe5jxX+FEsQTpjHLzvOGPn6ypFQprcs3jcuIWv1Xp/5v6P/rjfdvAwTkP2iF6pDx2tCd8vGLNWcsWzImTA=="],
"@prisma/dev": ["@prisma/dev@0.20.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", "@electric-sql/pglite-tools": "0.2.20", "@hono/node-server": "1.19.9", "@mrleebo/prisma-ast": "0.13.1", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "4.11.4", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ=="],
"@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-gzrUcbI9VmHS24Uf+0+7DNzdIw7keglJsD5m/MHxQOU68OhGVzlphQRobLiDMn8CHNA2XN8uugwKjudVtnfMVQ=="],
"@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-Wdlezh1ck0Rq2dDINkfSkwbR53q53//Eo1vVqVLwtiZ0I6fuWDGNPxwq+SNAIHnsU+FD/m3aIJKevH3vF13U3w=="],
"@prisma/engines": ["@prisma/engines@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0", "@prisma/engines-version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", "@prisma/fetch-engine": "7.2.0", "@prisma/get-platform": "7.2.0" } }, "sha512-HUeOI/SvCDsHrR9QZn24cxxZcujOjcS3w1oW/XVhnSATAli5SRMOfp/WkG3TtT5rCxDA4xOnlJkW7xkho4nURA=="],
"@prisma/engines": ["@prisma/engines@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0", "@prisma/engines-version": "7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", "@prisma/fetch-engine": "7.3.0", "@prisma/get-platform": "7.3.0" } }, "sha512-cWRQoPDXPtR6stOWuWFZf9pHdQ/o8/QNWn0m0zByxf5Kd946Q875XdEJ52pEsX88vOiXUmjuPG3euw82mwQNMg=="],
"@prisma/engines-version": ["@prisma/engines-version@7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", "", {}, "sha512-KezsjCZDsbjNR7SzIiVlUsn9PnLePI7r5uxABlwL+xoerurZTfgQVbIjvjF2sVr3Uc0ZcsnREw3F84HvbggGdA=="],
"@prisma/engines-version": ["@prisma/engines-version@7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", "", {}, "sha512-IH2va2ouUHihyiTTRW889LjKAl1CusZOvFfZxCDNpjSENt7g2ndFsK0vdIw/72v7+jCN6YgkHmdAP/BI7SDgyg=="],
"@prisma/fetch-engine": ["@prisma/fetch-engine@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0", "@prisma/engines-version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", "@prisma/get-platform": "7.2.0" } }, "sha512-Z5XZztJ8Ap+wovpjPD2lQKnB8nWFGNouCrglaNFjxIWAGWz0oeHXwUJRiclIoSSXN/ptcs9/behptSk8d0Yy6w=="],
"@prisma/fetch-engine": ["@prisma/fetch-engine@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0", "@prisma/engines-version": "7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", "@prisma/get-platform": "7.3.0" } }, "sha512-Mm0F84JMqM9Vxk70pzfNpGJ1lE4hYjOeLMu7nOOD1i83nvp8MSAcFYBnHqLvEZiA6onUR+m8iYogtOY4oPO5lQ=="],
"@prisma/get-platform": ["@prisma/get-platform@6.8.2", "", { "dependencies": { "@prisma/debug": "6.8.2" } }, "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow=="],
"@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="],
"@prisma/query-plan-executor": ["@prisma/query-plan-executor@6.18.0", "", {}, "sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA=="],
"@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="],
"@prisma/studio-core": ["@prisma/studio-core@0.9.0", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-xA2zoR/ADu/NCSQuriBKTh6Ps4XjU0bErkEcgMfnSGh346K1VI7iWKnoq1l2DoxUqiddPHIEWwtxJ6xCHG6W7g=="],
"@prisma/studio-core": ["@prisma/studio-core@0.13.1", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
@ -398,6 +406,8 @@
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
@ -438,6 +448,8 @@
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
@ -478,75 +490,75 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw=="],
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw=="],
"@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="],
"@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.1", "", { "dependencies": { "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ=="],
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg=="],
"@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.20.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.8", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ=="],
"@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.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA=="],
"@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/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw=="],
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.7", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-ujzPk8seYoDBmABDE5YqlhQZAXLOrtxtJLrbhHMKjBoG5b4dK4i6/mEU+6/7yXIAkqOO8sJ6YxZl+h0QQ1IJ7g=="],
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.8", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw=="],
"@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-x7BtAiIPSaNaWuzm24Q/mtSkv+BrISO/fmheiJ39PKRNH3RmH2Hph/bUKSOBOBC9unqfIYDhKTHwpyZycLGPVQ=="],
"@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ=="],
"@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.7", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-roySCtHC5+pQq5lK4be1fZ/WR6s/AxnPaLfCODIPArtN2du8s5Ot4mKVK3pPtijL/L654ws592JHJ1PbZFF6+A=="],
"@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.8", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A=="],
"@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.7", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-QVD+g3+icFkThoy4r8wVFZMsIP08taHVKjE6Jpmz8h5CgX/kk6pTODq5cht0OMtcapUx+xrPzUTQdA+TmO0m1g=="],
"@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.8", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA=="],
"@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.8", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-07InZontqsM1ggTCPSRgI7d8DirqRrnpL7nIACT4PW0AWrgDiHhjGZzbAE5UtRSiU0NISGUYe7/rri9ZeWyDpw=="],
"@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.9", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg=="],
"@smithy/hash-node": ["@smithy/hash-node@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw=="],
"@smithy/hash-node": ["@smithy/hash-node@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA=="],
"@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZQVoAwNYnFMIbd4DUc517HuwNelJUY6YOzwqrbcAgCnVn+79/OK7UjwA93SPpdTOpKDVkLIzavWm/Ck7SmnDPQ=="],
"@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w=="],
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ=="],
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ=="],
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="],
"@smithy/md5-js": ["@smithy/md5-js@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Wv6JcUxtOLTnxvNjDnAiATUsk8gvA6EeS8zzHig07dotpByYsLot+m0AaQEniUBjx97AC41MQR4hW0baraD1Xw=="],
"@smithy/md5-js": ["@smithy/md5-js@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ=="],
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg=="],
"@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.1", "", { "dependencies": { "@smithy/core": "^3.20.0", "@smithy/middleware-serde": "^4.2.8", "@smithy/node-config-provider": "^4.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg=="],
"@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.17", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/service-error-classification": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg=="],
"@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.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w=="],
"@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-stack": ["@smithy/middleware-stack@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw=="],
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA=="],
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.7", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw=="],
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.8", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.8", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg=="],
"@smithy/property-provider": ["@smithy/property-provider@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA=="],
"@smithy/property-provider": ["@smithy/property-provider@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w=="],
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA=="],
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ=="],
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg=="],
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw=="],
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w=="],
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA=="],
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0" } }, "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA=="],
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0" } }, "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ=="],
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.2", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg=="],
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.3", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.7", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg=="],
"@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.2", "", { "dependencies": { "@smithy/core": "^3.20.0", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-stack": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g=="],
"@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.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="],
"@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="],
"@smithy/url-parser": ["@smithy/url-parser@4.2.7", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg=="],
"@smithy/url-parser": ["@smithy/url-parser@4.2.8", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA=="],
"@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="],
@ -558,25 +570,25 @@
"@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.16", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ=="],
"@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.19", "", { "dependencies": { "@smithy/config-resolver": "^4.4.5", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA=="],
"@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.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg=="],
"@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-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="],
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w=="],
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A=="],
"@smithy/util-retry": ["@smithy/util-retry@4.2.7", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg=="],
"@smithy/util-retry": ["@smithy/util-retry@4.2.8", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg=="],
"@smithy/util-stream": ["@smithy/util-stream@4.5.8", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w=="],
"@smithy/util-stream": ["@smithy/util-stream@4.5.10", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g=="],
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="],
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
"@smithy/util-waiter": ["@smithy/util-waiter@4.2.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw=="],
"@smithy/util-waiter": ["@smithy/util-waiter@4.2.8", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg=="],
"@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
@ -642,13 +654,13 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
"@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="],
"@types/nodemailer": ["@types/nodemailer@7.0.4", "", { "dependencies": { "@aws-sdk/client-sesv2": "^3.839.0", "@types/node": "*" } }, "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow=="],
"@types/nodemailer": ["@types/nodemailer@7.0.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow=="],
"@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.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
"@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=="],
@ -664,27 +676,27 @@
"@udecode/utils": ["@udecode/utils@52.0.1", "", {}, "sha512-T/qa4++J2MMY9CbhZ05ONpcpkvsRV0uBvOfMhhYWKkAND4DSVAVzmZTV7WQ5fjGq81agAped0WQxcqsEg617uQ=="],
"@vibrant/color": ["@vibrant/color@4.0.0", "", {}, "sha512-S9ItdqS1135wTXoIIqAJu8df9dqlOo6Boc5Y4MGsBTu9UmUOvOwfj5b4Ga6S5yrLAKmKYIactkz7zYJdMddkig=="],
"@vibrant/color": ["@vibrant/color@4.0.4", "", {}, "sha512-Fq2tAszz4QOPWfHZ+KuEAchXUD8i594BM2fOJt8dI/fvYbiVoBycBF/BlNH6F4IWBubxXoPqD4JmmAHvFYbNew=="],
"@vibrant/core": ["@vibrant/core@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/generator": "^4.0.0", "@vibrant/image": "^4.0.0", "@vibrant/quantizer": "^4.0.0", "@vibrant/worker": "^4.0.0" } }, "sha512-fqlVRUTDjEws9VNKvI3cDXM4wUT7fMFS+cVqEjJk3im+R5EvjJzPF6OAbNhfPzW04NvHNE555eY9FfhYuX3PRw=="],
"@vibrant/core": ["@vibrant/core@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4", "@vibrant/generator": "^4.0.4", "@vibrant/image": "^4.0.4", "@vibrant/quantizer": "^4.0.4", "@vibrant/worker": "^4.0.4" } }, "sha512-yZ0XSpW2biKyaJPpBC31AVYgn7NseKSO2q3KNMmDrkL2qC6TEWsBMnSQ28n0m///chZELXpQLx1CCOsWg5pj8w=="],
"@vibrant/generator": ["@vibrant/generator@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/types": "^4.0.0" } }, "sha512-CqKAjmgHVDXJVo3Q5+9pUJOvksR7cN3bzx/6MbURYh7lA4rhsIewkUK155M6q0vfcUN3ETi/eTneCi0tLuM2Sg=="],
"@vibrant/generator": ["@vibrant/generator@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4", "@vibrant/types": "^4.0.4" } }, "sha512-rwq8PnlpKdch4YqaA1FAwdm71gKE2cMrUsbu72TqRFGa8rpP1roaZlQCVXIIwElXVc3r9axZyAcqyTLaMjhrTg=="],
"@vibrant/generator-default": ["@vibrant/generator-default@4.0.3", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/generator": "^4.0.0" } }, "sha512-HZlfp19sDokODEkZF4p70QceARHgjP3a1Dmxg+dlblYMJM98jPq+azA0fzqKNR7R17JJNHxexpJEepEsNlG0gw=="],
"@vibrant/generator-default": ["@vibrant/generator-default@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4", "@vibrant/generator": "^4.0.4" } }, "sha512-QeVDeH2dz9lityvJCb84Ml4hlBTElwCpU7SVpiDFBh6gPoCLnzcb1H9G4NgG3hOlAPyrBM+Ivq1Pg+1lZj5Ywg=="],
"@vibrant/image": ["@vibrant/image@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0" } }, "sha512-Asv/7R/L701norosgvbjOVkodFiwcFihkXixA/gbAd6C+5GCts1Wm1NPk14FNKnM7eKkfAN+0wwPkdOH+PY/YA=="],
"@vibrant/image": ["@vibrant/image@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4" } }, "sha512-NBIJj7umfDRVpFjJHQo1AFSCWCzQyjfil+Yxu7W62PEL72GPCif0CDiglPkvVF8QhDLmnx/x1k3LIBb9jWF2sw=="],
"@vibrant/image-browser": ["@vibrant/image-browser@4.0.0", "", { "dependencies": { "@vibrant/image": "^4.0.0" } }, "sha512-mXckzvJWiP575Y/wNtP87W/TPgyJoGlPBjW4E9YmNS6n4Jb6RqyHQA0ZVulqDslOxjSsihDzY7gpAORRclaoLg=="],
"@vibrant/image-browser": ["@vibrant/image-browser@4.0.4", "", { "dependencies": { "@vibrant/image": "^4.0.4" } }, "sha512-7qVyAm+z9t98iwMDzUgGCwgRg0KBB5RXQFgiO2Um5Izd1wO7BKC0SHVEz2k7sRx3XNfBf+JExp8quPrvSz17gg=="],
"@vibrant/image-node": ["@vibrant/image-node@4.0.0", "", { "dependencies": { "@jimp/custom": "^0.22.12", "@jimp/plugin-resize": "^0.22.12", "@jimp/types": "^0.22.12", "@vibrant/image": "^4.0.0" } }, "sha512-m7yfnQtmo2y8z+tOjRFBx6q/qGnhl/ax2uCaj4TBkm4TtXfR4Dsn90wT6OWXmCFFzxIKHXKKEBShkxR+4RHseA=="],
"@vibrant/image-node": ["@vibrant/image-node@4.0.4", "", { "dependencies": { "@jimp/custom": "^0.22.12", "@jimp/plugin-resize": "^0.22.12", "@jimp/types": "^0.22.12", "@vibrant/image": "^4.0.4" } }, "sha512-aG8Ukt9oTa6FWaAV5oBKsBetkKASWH31hZiFJ2R1291f3TZlphUyQTJz5TubucIRsCEl4dgG1xyxFPgse2IABA=="],
"@vibrant/quantizer": ["@vibrant/quantizer@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/image": "^4.0.0", "@vibrant/types": "^4.0.0" } }, "sha512-YDGxmCv/RvHFtZghDlVRwH5GMxdGGozWS1JpUOUt73/F5zAKGiiier8F31K1npIXARn6/Gspvg/Rbg7qqyEr2A=="],
"@vibrant/quantizer": ["@vibrant/quantizer@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4", "@vibrant/image": "^4.0.4", "@vibrant/types": "^4.0.4" } }, "sha512-722CooC2W4mlBiv+zyAsIrIvARnMCN/P2Muo8bnWd0SQlVWFtQnFxJWGOUPOPS4DGe3pGoqmNfvS0let4dICZQ=="],
"@vibrant/quantizer-mmcq": ["@vibrant/quantizer-mmcq@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/image": "^4.0.0", "@vibrant/quantizer": "^4.0.0" } }, "sha512-TZqNiRoGGyCP8fH1XE6rvhFwLNv9D8MP1Xhz3K8tsuUweC6buWax3qLfrfEnkhtQnPJHaqvTfTOlIIXVMfRpow=="],
"@vibrant/quantizer-mmcq": ["@vibrant/quantizer-mmcq@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4", "@vibrant/image": "^4.0.4", "@vibrant/quantizer": "^4.0.4" } }, "sha512-/1CNnM96J8K+OBCWNUzywo6VdnmdFJyiKO+ty/nkfe8H0NseOEHIL7PrVtWGgtsb0rh2uTAq2rjXv65TfgPy8g=="],
"@vibrant/types": ["@vibrant/types@4.0.0", "", {}, "sha512-tA5TAbuROXcPkt+PWjmGfoaiEXyySVaNnCZovf6vXhCbMdrTTCQXvNCde2geiVl6YwtuU/Qrj9iZxS5jZ6yVIw=="],
"@vibrant/types": ["@vibrant/types@4.0.4", "", {}, "sha512-Qq3mVTJamn7yD4OBgBEUKaxfDlm3sxBK55N7dH3XzI9Ey7KR00R06uwtqOcEJMsziWTEXdYN3VUlYaj2Tkt7hw=="],
"@vibrant/worker": ["@vibrant/worker@4.0.0", "", { "dependencies": { "@vibrant/types": "^4.0.0" } }, "sha512-nSaZZwWQKOgN/nPYUAIRF0/uoa7KpK91A+gjLmZZDgfN1enqxaiihmn+75ayNadW0c6cxAEpEFEHTONR5u9tMw=="],
"@vibrant/worker": ["@vibrant/worker@4.0.4", "", { "dependencies": { "@vibrant/types": "^4.0.4" } }, "sha512-Q/R6PYhSMWCXEk/IcXbWIzIu7Z4b58ABkGvcdF8Y+q/7g+KnpxKW5x/jfQ/6ciyYSby13wZZoEdNr3QQVgsdBQ=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
@ -720,9 +732,9 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
"better-auth": ["better-auth@1.4.10", "", { "dependencies": { "@better-auth/core": "1.4.10", "@better-auth/telemetry": "1.4.10", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg=="],
"better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="],
"better-call": ["better-call@1.1.7", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ=="],
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
"bmp-js": ["bmp-js@0.1.0", "", {}, "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="],
@ -858,7 +870,7 @@
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-port-please": ["get-port-please@3.1.2", "", {}, "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ=="],
"get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="],
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
@ -870,9 +882,11 @@
"grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="],
"graphmatch": ["graphmatch@1.1.0", "", {}, "sha512-0E62MaTW5rPZVRLyIJZG/YejmdA/Xr1QydHEw3Vt+qOKkMIOE8WDLc9ZX2bmAjtJFZcId4lEdrdmASsEy7D1QA=="],
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
"hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="],
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
"html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="],
@ -1102,7 +1116,7 @@
"nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="],
"next": ["next@16.1.1", "", { "dependencies": { "@next/env": "16.1.1", "@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.1", "@next/swc-darwin-x64": "16.1.1", "@next/swc-linux-arm64-gnu": "16.1.1", "@next/swc-linux-arm64-musl": "16.1.1", "@next/swc-linux-x64-gnu": "16.1.1", "@next/swc-linux-x64-musl": "16.1.1", "@next/swc-win32-arm64-msvc": "16.1.1", "@next/swc-win32-x64-msvc": "16.1.1", "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-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w=="],
"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=="],
@ -1110,9 +1124,9 @@
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-vibrant": ["node-vibrant@4.0.3", "", { "dependencies": { "@types/node": "^18.15.3", "@vibrant/core": "^4.0.0", "@vibrant/generator-default": "^4.0.3", "@vibrant/image-browser": "^4.0.0", "@vibrant/image-node": "^4.0.0", "@vibrant/quantizer-mmcq": "^4.0.0" } }, "sha512-kzoIuJK90BH/k65Avt077JCX4Nhqz1LNc8cIOm2rnYEvFdJIYd8b3SQwU1MTpzcHtr8z8jxkl1qdaCfbP3olFg=="],
"node-vibrant": ["node-vibrant@4.0.4", "", { "dependencies": { "@types/node": "^18.15.3", "@vibrant/core": "^4.0.4", "@vibrant/generator-default": "^4.0.4", "@vibrant/image-browser": "^4.0.4", "@vibrant/image-node": "^4.0.4", "@vibrant/quantizer-mmcq": "^4.0.4" } }, "sha512-hA/pUXBE9TJ41G9FlTkzeqD5JdxgvvPGYZb/HNpdkaxxXUEnP36imSolZ644JuPun+lTd+FpWWtBpTYdp2noQA=="],
"nodemailer": ["nodemailer@7.0.12", "", {}, "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA=="],
"nodemailer": ["nodemailer@7.0.13", "", {}, "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
@ -1140,15 +1154,15 @@
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="],
"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.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
"pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
"pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="],
"pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
@ -1162,7 +1176,7 @@
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"platejs": ["platejs@52.0.15", "", { "dependencies": { "@platejs/core": "52.0.15", "@platejs/slate": "52.0.10", "@platejs/utils": "52.0.15", "@udecode/react-hotkeys": "52.0.11", "@udecode/react-utils": "52.0.11", "@udecode/utils": "52.0.1", "react-compiler-runtime": "^1.0.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-SaNmVAKGfmTLUIYfK3wxk91w/4oQCHwgrMVjUR+oe75yL0WeoC+HPvSl9sovw0A3tLaHdZ7ZwsWLSjR0the9+A=="],
"platejs": ["platejs@52.0.17", "", { "dependencies": { "@platejs/core": "52.0.17", "@platejs/slate": "52.0.10", "@platejs/utils": "52.0.17", "@udecode/react-hotkeys": "52.0.11", "@udecode/react-utils": "52.0.11", "@udecode/utils": "52.0.1", "react-compiler-runtime": "^1.0.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-vJZijt8coKh6W60RmUG69DFM4ZwLhzNwSfOwfk5SkVWer1iDqqyW34JnGn5nKI8irj+45/sSzx4Nfgw9CR+QYw=="],
"pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
@ -1178,7 +1192,7 @@
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"prisma": ["prisma@7.2.0", "", { "dependencies": { "@prisma/config": "7.2.0", "@prisma/dev": "0.17.0", "@prisma/engines": "7.2.0", "@prisma/studio-core": "0.9.0", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-jSdHWgWOgFF24+nRyyNRVBIgGDQEsMEF8KPHvhBBg3jWyR9fUAK0Nq9ThUmiGlNgq2FA7vSk/ZoCvefod+a8qg=="],
"prisma": ["prisma@7.3.0", "", { "dependencies": { "@prisma/config": "7.3.0", "@prisma/dev": "0.20.0", "@prisma/engines": "7.3.0", "@prisma/studio-core": "0.13.1", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-ApYSOLHfMN8WftJA+vL6XwAPOh/aZ0BgUyyKPwUFgjARmG6EBI9LzDPf6SWULQMSAxydV9qn5gLj037nPNlg2w=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
@ -1192,15 +1206,15 @@
"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-compiler-runtime": ["react-compiler-runtime@1.0.0", "", { "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" } }, "sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w=="],
"react-day-picker": ["react-day-picker@9.13.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ=="],
"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.69.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw=="],
"react-hook-form": ["react-hook-form@7.71.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
@ -1232,7 +1246,7 @@
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"remeda": ["remeda@2.21.3", "", { "dependencies": { "type-fest": "^4.39.1" } }, "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg=="],
"remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
@ -1276,7 +1290,7 @@
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="],
@ -1330,8 +1344,6 @@
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
@ -1386,11 +1398,11 @@
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"zeptomatch": ["zeptomatch@2.0.2", "", { "dependencies": { "grammex": "^3.1.10" } }, "sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g=="],
"zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="],
"zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="],
"zod": ["zod@4.3.4", "", {}, "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zustand": ["zustand@5.0.9", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg=="],
@ -1416,17 +1428,17 @@
"@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/client-sesv2/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.958.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.957.0", "@aws-sdk/credential-provider-http": "3.957.0", "@aws-sdk/credential-provider-ini": "3.958.0", "@aws-sdk/credential-provider-process": "3.957.0", "@aws-sdk/credential-provider-sso": "3.958.0", "@aws-sdk/credential-provider-web-identity": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-vdoZbNG2dt66I7EpN3fKCzi6fp9xjIiwEA/vVVgqO4wXCGw8rKPIdDUus4e13VvTr330uQs2W0UNg/7AgtquEQ=="],
"@jimp/core/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"@platejs/core/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="],
"@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=="],
"@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="],
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="],
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@6.8.2", "", {}, "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg=="],
"@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="],
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="],
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
@ -1450,6 +1462,10 @@
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@types/pg/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
"@types/readdir-glob/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
"c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
@ -1466,6 +1482,8 @@
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"pg/pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
"pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"pixelmatch/pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="],
@ -1502,7 +1520,9 @@
"@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/client-sesv2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.958.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/credential-provider-env": "3.957.0", "@aws-sdk/credential-provider-http": "3.957.0", "@aws-sdk/credential-provider-login": "3.958.0", "@aws-sdk/credential-provider-process": "3.957.0", "@aws-sdk/credential-provider-sso": "3.958.0", "@aws-sdk/credential-provider-web-identity": "3.958.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-u7twvZa1/6GWmPBZs6DbjlegCoNzNjBsMS/6fvh5quByYrcJr/uLd8YEr7S3UIq4kR/gSnHqcae7y2nL2bqZdg=="],
"@prisma/adapter-pg/pg/pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="],
"@prisma/adapter-pg/pg/pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
"lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
@ -1521,7 +1541,5 @@
"@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-sdk/client-sesv2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.958.0", "", { "dependencies": { "@aws-sdk/core": "3.957.0", "@aws-sdk/nested-clients": "3.958.0", "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-sDwtDnBSszUIbzbOORGh5gmXGl9aK25+BHb4gb1aVlqB+nNL2+IUEJA62+CE55lXSH8qXF90paivjK8tOHTwPA=="],
}
}

View File

@ -19,6 +19,7 @@
"hooks": "@/hooks"
},
"registries": {
"@plate": "https://platejs.org/r/{name}.json"
"@plate": "https://platejs.org/r/{name}.json",
"@diceui": "https://diceui.com/r/{name}.json"
}
}

View File

@ -9,6 +9,7 @@ module.exports = {
serverActions: {
bodySizeLimit: '50mb',
},
proxyClientMaxBodySize: '50mb',
},
output: "standalone",
}

8882
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,41 @@
{
"name": "admin.gaertan.art",
"version": "0.1.0",
"version": "1.1.0",
"private": true,
"scripts": {
"dev": "PORT=3001 NODE_OPTIONS='--max-old-space-size=4096' next dev",
"dev:version": "NEXT_PUBLIC_GIT_SHA=$(git rev-parse --short HEAD) NEXT_PUBLIC_DEPLOY_ENV=dev PORT=3001 NODE_OPTIONS='--max-old-space-size=4096' next dev",
"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",
"lint": "biome check",
"format": "biome format --write"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.962.0",
"@aws-sdk/s3-request-presigner": "^3.962.0",
"@aws-sdk/client-s3": "^3.980.0",
"@aws-sdk/s3-request-presigner": "^3.980.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.2.2",
"@platejs/basic-nodes": "^52.0.11",
"@platejs/code-block": "^52.0.11",
"@platejs/indent": "^52.0.11",
"@platejs/list": "^52.0.11",
"@platejs/markdown": "^52.0.11",
"@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0",
"@platejs/markdown": "^52.1.0",
"@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.3.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
@ -40,7 +46,7 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"archiver": "^7.0.1",
"better-auth": "^1.4.10",
"better-auth": "^1.4.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@ -49,23 +55,23 @@
"dotenv": "^17.2.3",
"lowlight": "^3.3.0",
"lucide-react": "^0.561.0",
"next": "^16.1.1",
"next": "16.1.6",
"next-themes": "^0.4.6",
"node-vibrant": "^4.0.3",
"nodemailer": "^7.0.12",
"pg": "^8.16.3",
"platejs": "^52.0.15",
"react": "19.2.1",
"node-vibrant": "^4.0.4",
"nodemailer": "^7.0.13",
"pg": "^8.18.0",
"platejs": "^52.0.17",
"react": "19.2.4",
"react-day-picker": "^9.13.0",
"react-dom": "19.2.1",
"react-hook-form": "^7.69.0",
"react-dom": "19.2.4",
"react-hook-form": "^7.71.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwind-scrollbar-hide": "^4.0.0",
"uuid": "^13.0.0",
"zod": "^4.3.4"
"zod": "^4.3.6"
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
@ -73,15 +79,19 @@
"@types/archiver": "^7.0.0",
"@types/culori": "^4.0.1",
"@types/date-fns": "^2.6.3",
"@types/node": "^20.19.27",
"@types/nodemailer": "^7.0.4",
"@types/node": "^20.19.30",
"@types/nodemailer": "^7.0.9",
"@types/pg": "^8.16.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react": "19.2.10",
"@types/react-dom": "19.2.3",
"@types/uuid": "^11.0.0",
"prisma": "^7.2.0",
"prisma": "^7.3.0",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3"
},
"overrides": {
"@types/react": "19.2.10",
"@types/react-dom": "19.2.3"
}
}

View File

@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "ArtworkTimelapse" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"artworkId" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT false,
"s3Key" TEXT NOT NULL,
"fileName" TEXT,
"mimeType" TEXT,
"sizeBytes" INTEGER,
CONSTRAINT "ArtworkTimelapse_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ArtworkTimelapse_artworkId_key" ON "ArtworkTimelapse"("artworkId");
-- CreateIndex
CREATE UNIQUE INDEX "ArtworkTimelapse_s3Key_key" ON "ArtworkTimelapse"("s3Key");
-- AddForeignKey
ALTER TABLE "ArtworkTimelapse" ADD CONSTRAINT "ArtworkTimelapse_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "CommissionGuidelines" ADD COLUMN "exampleImageUrl" TEXT;

View File

@ -0,0 +1,65 @@
-- CreateTable
CREATE TABLE "CommissionCustomCard" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"sortIndex" INTEGER NOT NULL DEFAULT 0,
"name" TEXT NOT NULL,
"description" TEXT,
"referenceImageUrl" TEXT,
"isVisible" BOOLEAN NOT NULL DEFAULT true,
"isSpecialOffer" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "CommissionCustomCard_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CommissionCustomCardOption" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"sortIndex" INTEGER NOT NULL DEFAULT 0,
"cardId" TEXT NOT NULL,
"optionId" TEXT NOT NULL,
"priceRange" TEXT,
"pricePercent" DOUBLE PRECISION,
"price" DOUBLE PRECISION,
CONSTRAINT "CommissionCustomCardOption_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CommissionCustomCardExtra" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"sortIndex" INTEGER NOT NULL DEFAULT 0,
"cardId" TEXT NOT NULL,
"extraId" TEXT NOT NULL,
"priceRange" TEXT,
"pricePercent" DOUBLE PRECISION,
"price" DOUBLE PRECISION,
CONSTRAINT "CommissionCustomCardExtra_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "CommissionCustomCard_isVisible_sortIndex_idx" ON "CommissionCustomCard"("isVisible", "sortIndex");
-- CreateIndex
CREATE UNIQUE INDEX "CommissionCustomCardOption_cardId_optionId_key" ON "CommissionCustomCardOption"("cardId", "optionId");
-- CreateIndex
CREATE UNIQUE INDEX "CommissionCustomCardExtra_cardId_extraId_key" ON "CommissionCustomCardExtra"("cardId", "extraId");
-- AddForeignKey
ALTER TABLE "CommissionCustomCardOption" ADD CONSTRAINT "CommissionCustomCardOption_cardId_fkey" FOREIGN KEY ("cardId") REFERENCES "CommissionCustomCard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CommissionCustomCardOption" ADD CONSTRAINT "CommissionCustomCardOption_optionId_fkey" FOREIGN KEY ("optionId") REFERENCES "CommissionOption"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CommissionCustomCardExtra" ADD CONSTRAINT "CommissionCustomCardExtra_cardId_fkey" FOREIGN KEY ("cardId") REFERENCES "CommissionCustomCard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CommissionCustomCardExtra" ADD CONSTRAINT "CommissionCustomCardExtra_extraId_fkey" FOREIGN KEY ("extraId") REFERENCES "CommissionExtra"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "CommissionRequest" ADD COLUMN "customCardId" TEXT;
-- AddForeignKey
ALTER TABLE "CommissionRequest" ADD CONSTRAINT "CommissionRequest_customCardId_fkey" FOREIGN KEY ("customCardId") REFERENCES "CommissionCustomCard"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,140 @@
-- CreateTable
CREATE TABLE "Tag" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"sortIndex" INTEGER NOT NULL DEFAULT 0,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"isVisible" BOOLEAN NOT NULL DEFAULT true,
"description" TEXT,
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TagAlias" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"alias" TEXT NOT NULL,
"tagId" TEXT NOT NULL,
CONSTRAINT "TagAlias_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TagCategory" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"tagId" TEXT NOT NULL,
"categoryId" TEXT NOT NULL,
"isParent" BOOLEAN NOT NULL DEFAULT false,
"showOnAnimalPage" BOOLEAN NOT NULL DEFAULT false,
"parentTagId" TEXT,
CONSTRAINT "TagCategory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Miniature" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Miniature_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_ArtworkTagsV2" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ArtworkTagsV2_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_MiniatureTags" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_MiniatureTags_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_CommissionTypeTags" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_CommissionTypeTags_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Tag_slug_key" ON "Tag"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "TagAlias_alias_key" ON "TagAlias"("alias");
-- CreateIndex
CREATE INDEX "TagAlias_alias_idx" ON "TagAlias"("alias");
-- CreateIndex
CREATE UNIQUE INDEX "TagAlias_tagId_alias_key" ON "TagAlias"("tagId", "alias");
-- CreateIndex
CREATE INDEX "TagCategory_categoryId_idx" ON "TagCategory"("categoryId");
-- CreateIndex
CREATE INDEX "TagCategory_tagId_idx" ON "TagCategory"("tagId");
-- CreateIndex
CREATE INDEX "TagCategory_parentTagId_idx" ON "TagCategory"("parentTagId");
-- CreateIndex
CREATE INDEX "TagCategory_categoryId_parentTagId_idx" ON "TagCategory"("categoryId", "parentTagId");
-- CreateIndex
CREATE UNIQUE INDEX "TagCategory_tagId_categoryId_key" ON "TagCategory"("tagId", "categoryId");
-- CreateIndex
CREATE INDEX "_ArtworkTagsV2_B_index" ON "_ArtworkTagsV2"("B");
-- CreateIndex
CREATE INDEX "_MiniatureTags_B_index" ON "_MiniatureTags"("B");
-- CreateIndex
CREATE INDEX "_CommissionTypeTags_B_index" ON "_CommissionTypeTags"("B");
-- AddForeignKey
ALTER TABLE "TagAlias" ADD CONSTRAINT "TagAlias_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TagCategory" ADD CONSTRAINT "TagCategory_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TagCategory" ADD CONSTRAINT "TagCategory_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "ArtCategory"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TagCategory" ADD CONSTRAINT "TagCategory_parentTagId_fkey" FOREIGN KEY ("parentTagId") REFERENCES "Tag"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ArtworkTagsV2" ADD CONSTRAINT "_ArtworkTagsV2_A_fkey" FOREIGN KEY ("A") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ArtworkTagsV2" ADD CONSTRAINT "_ArtworkTagsV2_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_MiniatureTags" ADD CONSTRAINT "_MiniatureTags_A_fkey" FOREIGN KEY ("A") REFERENCES "Miniature"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_MiniatureTags" ADD CONSTRAINT "_MiniatureTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_CommissionTypeTags" ADD CONSTRAINT "_CommissionTypeTags_A_fkey" FOREIGN KEY ("A") REFERENCES "CommissionType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_CommissionTypeTags" ADD CONSTRAINT "_CommissionTypeTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,65 @@
/*
Warnings:
- You are about to drop the `ArtTag` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `ArtTagAlias` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `_ArtCategoryToArtTag` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `_ArtTagToArtwork` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `_ArtworkTagsV2` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "ArtTag" DROP CONSTRAINT "ArtTag_parentId_fkey";
-- DropForeignKey
ALTER TABLE "ArtTagAlias" DROP CONSTRAINT "ArtTagAlias_tagId_fkey";
-- DropForeignKey
ALTER TABLE "_ArtCategoryToArtTag" DROP CONSTRAINT "_ArtCategoryToArtTag_A_fkey";
-- DropForeignKey
ALTER TABLE "_ArtCategoryToArtTag" DROP CONSTRAINT "_ArtCategoryToArtTag_B_fkey";
-- DropForeignKey
ALTER TABLE "_ArtTagToArtwork" DROP CONSTRAINT "_ArtTagToArtwork_A_fkey";
-- DropForeignKey
ALTER TABLE "_ArtTagToArtwork" DROP CONSTRAINT "_ArtTagToArtwork_B_fkey";
-- DropForeignKey
ALTER TABLE "_ArtworkTagsV2" DROP CONSTRAINT "_ArtworkTagsV2_A_fkey";
-- DropForeignKey
ALTER TABLE "_ArtworkTagsV2" DROP CONSTRAINT "_ArtworkTagsV2_B_fkey";
-- DropTable
DROP TABLE "ArtTag";
-- DropTable
DROP TABLE "ArtTagAlias";
-- DropTable
DROP TABLE "_ArtCategoryToArtTag";
-- DropTable
DROP TABLE "_ArtTagToArtwork";
-- DropTable
DROP TABLE "_ArtworkTagsV2";
-- CreateTable
CREATE TABLE "_ArtworkTags" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ArtworkTags_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_ArtworkTags_B_index" ON "_ArtworkTags"("B");
-- AddForeignKey
ALTER TABLE "_ArtworkTags" ADD CONSTRAINT "_ArtworkTags_A_fkey" FOREIGN KEY ("A") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ArtworkTags" ADD CONSTRAINT "_ArtworkTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "_CommissionCustomCardTags" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_CommissionCustomCardTags_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_CommissionCustomCardTags_B_index" ON "_CommissionCustomCardTags"("B");
-- AddForeignKey
ALTER TABLE "_CommissionCustomCardTags" ADD CONSTRAINT "_CommissionCustomCardTags_A_fkey" FOREIGN KEY ("A") REFERENCES "CommissionCustomCard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_CommissionCustomCardTags" ADD CONSTRAINT "_CommissionCustomCardTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -47,12 +47,13 @@ model Artwork {
galleryId String?
gallery Gallery? @relation(fields: [galleryId], references: [id])
metadata ArtworkMetadata?
metadata ArtworkMetadata?
timelapse ArtworkTimelapse?
albums Album[]
categories ArtCategory[]
colors ArtworkColor[]
tags ArtTag[]
tags Tag[] @relation("ArtworkTags")
variants FileVariant[]
@@index([colorStatus])
@ -100,43 +101,7 @@ model ArtCategory {
description String?
artworks Artwork[]
tags ArtTag[]
}
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])
tagLinks TagCategory[]
}
model Color {
@ -195,6 +160,22 @@ model ArtworkMetadata {
artwork Artwork @relation(fields: [artworkId], references: [id])
}
model ArtworkTimelapse {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
artworkId String @unique
artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade)
enabled Boolean @default(false)
s3Key String @unique
fileName String?
mimeType String?
sizeBytes Int?
}
model FileData {
id String @id @default(cuid())
createdAt DateTime @default(now())
@ -231,6 +212,72 @@ model FileVariant {
@@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 {
id String @id @default(cuid())
createdAt DateTime @default(now())
@ -248,12 +295,35 @@ model CommissionType {
description String?
tags Tag[] @relation("CommissionTypeTags")
options CommissionTypeOption[]
extras CommissionTypeExtra[]
customInputs CommissionTypeCustomInput[]
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 {
id String @id @default(cuid())
createdAt DateTime @default(now())
@ -264,8 +334,9 @@ model CommissionOption {
description String?
types CommissionTypeOption[]
requests CommissionRequest[]
types CommissionTypeOption[]
customCards CommissionCustomCardOption[]
requests CommissionRequest[]
}
model CommissionTypeOption {
@ -297,8 +368,9 @@ model CommissionExtra {
description String?
requests CommissionRequest[]
types CommissionTypeExtra[]
requests CommissionRequest[]
types CommissionTypeExtra[]
customCards CommissionCustomCardExtra[]
}
model CommissionTypeExtra {
@ -320,6 +392,25 @@ model CommissionTypeExtra {
@@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 {
id String @id @default(cuid())
createdAt DateTime @default(now())
@ -351,6 +442,25 @@ model CommissionTypeCustomInput {
@@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 {
id String @id @default(cuid())
index Int @default(autoincrement())
@ -367,10 +477,12 @@ model CommissionRequest {
userAgent String?
customFields Json?
optionId String?
typeId String?
option CommissionOption? @relation(fields: [optionId], references: [id])
type CommissionType? @relation(fields: [typeId], references: [id])
optionId String?
typeId String?
customCardId String?
option CommissionOption? @relation(fields: [optionId], references: [id])
type CommissionType? @relation(fields: [typeId], references: [id])
customCard CommissionCustomCard? @relation(fields: [customCardId], references: [id])
extras CommissionExtra[]
files CommissionRequestFile[]
@ -381,8 +493,9 @@ model CommissionGuidelines {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
markdown String
isActive Boolean @default(true)
markdown String
exampleImageUrl String?
isActive Boolean @default(true)
@@index([isActive])
}

View File

@ -1,77 +1,77 @@
import { PrismaClient } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { hashPassword } from "better-auth/crypto";
// import { PrismaClient } from "@/generated/prisma/client";
// import { PrismaPg } from "@prisma/adapter-pg";
// import { hashPassword } from "better-auth/crypto";
const connectionString = `${process.env.DATABASE_URL}`
// const connectionString = `${process.env.DATABASE_URL}`
const adapter = new PrismaPg({ connectionString })
const prisma = new PrismaClient({ adapter })
// const adapter = new PrismaPg({ connectionString })
// const prisma = new PrismaClient({ adapter })
function uid() {
return crypto.randomUUID();
}
// function uid() {
// return crypto.randomUUID();
// }
async function main() {
const email = "admin@gaertan.art";
const name = "Admin";
const password = "037Ikk7qmCamW5iYBimcwiPXNELzktIRG9ndiIkA3u";
// async function main() {
// const email = "admin@gaertan.art";
// const name = "Admin";
// const password = "037Ikk7qmCamW5iYBimcwiPXNELzktIRG9ndiIkA3u";
const passwordHash = await hashPassword(password);
// const passwordHash = await hashPassword(password);
// 1) Ensure user exists (your User.id has no default, so we must set it)
const user = await prisma.user.upsert({
where: { email },
update: {
name,
// optional: mark verified for initial admin
emailVerified: true,
},
create: {
id: uid(),
email,
name,
emailVerified: true,
},
});
// // 1) Ensure user exists (your User.id has no default, so we must set it)
// const user = await prisma.user.upsert({
// where: { email },
// update: {
// name,
// // optional: mark verified for initial admin
// emailVerified: true,
// },
// create: {
// id: uid(),
// email,
// name,
// emailVerified: true,
// },
// });
// 2) Ensure credential account exists for this user
// Better Auth expects providerId="credential" and accountId=userId for credential accounts. :contentReference[oaicite:2]{index=2}
const existingCredential = await prisma.account.findFirst({
where: {
userId: user.id,
providerId: "credential",
accountId: user.id,
},
});
// // 2) Ensure credential account exists for this user
// // Better Auth expects providerId="credential" and accountId=userId for credential accounts. :contentReference[oaicite:2]{index=2}
// const existingCredential = await prisma.account.findFirst({
// where: {
// userId: user.id,
// providerId: "credential",
// accountId: user.id,
// },
// });
if (!existingCredential) {
await prisma.account.create({
data: {
id: uid(),
userId: user.id,
providerId: "credential",
accountId: user.id,
password: passwordHash,
},
});
} else {
// If it exists, keep it consistent (optionally reset password on seed)
await prisma.account.update({
where: { id: existingCredential.id },
data: { password: passwordHash },
});
}
// if (!existingCredential) {
// await prisma.account.create({
// data: {
// id: uid(),
// userId: user.id,
// providerId: "credential",
// accountId: user.id,
// password: passwordHash,
// },
// });
// } else {
// // If it exists, keep it consistent (optionally reset password on seed)
// await prisma.account.update({
// where: { id: existingCredential.id },
// data: { password: passwordHash },
// });
// }
console.log("Seeded/updated admin user:");
console.log(" email:", email);
console.log(" password:", password);
}
// console.log("Seeded/updated admin user:");
// console.log(" email:", email);
// console.log(" password:", password);
// }
main()
.catch((err) => {
console.error("Seed failed:", err);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
// main()
// .catch((err) => {
// console.error("Seed failed:", err);
// process.exit(1);
// })
// .finally(async () => {
// await prisma.$disconnect();
// });

View File

@ -9,6 +9,7 @@ export async function deleteArtwork(artworkId: string) {
where: { id: artworkId },
include: {
variants: true,
timelapse: true,
colors: true,
metadata: true,
tags: true,
@ -32,6 +33,20 @@ export async function deleteArtwork(artworkId: string) {
}
}
// Delete timelapse S3 object (if present)
if (artwork.timelapse?.s3Key) {
try {
await s3.send(
new DeleteObjectCommand({
Bucket: `${process.env.BUCKET_NAME}`,
Key: artwork.timelapse.s3Key,
})
);
} catch (err) {
console.warn(`Failed to delete timelapse S3 object: ${artwork.timelapse.s3Key}. ${err}`);
}
}
// Step 1: Delete join entries
await prisma.artworkColor.deleteMany({ where: { artworkId } });
@ -48,6 +63,9 @@ export async function deleteArtwork(artworkId: string) {
// Delete variants
await prisma.fileVariant.deleteMany({ where: { artworkId } });
// Delete timelapse DB row (relation also cascades, but be explicit)
await prisma.artworkTimelapse.deleteMany({ where: { artworkId } });
// Delete metadata
await prisma.artworkMetadata.deleteMany({ where: { artworkId } });
@ -64,4 +82,4 @@ export async function deleteArtwork(artworkId: string) {
await prisma.artwork.delete({ where: { id: artworkId } });
return { success: true };
}
}

View File

@ -1,36 +1,49 @@
"use server";
import { prisma } from "@/lib/prisma";
import { VibrantSwatch } from "@/types/VibrantSwatch";
import type { VibrantSwatch } from "@/types/VibrantSwatch";
import { getImageBufferFromS3Key } from "@/utils/getImageBufferFromS3";
import { generateColorName, rgbToHex } from "@/utils/uploadHelper";
import { converter, parse } from "culori";
import { Vibrant } from "node-vibrant/node";
const toOklab = converter("oklab");
const A_MIN = -0.5, A_MAX = 0.5;
const B_MIN = -0.5, B_MAX = 0.5;
const A_MIN = -0.5,
A_MAX = 0.5;
const B_MIN = -0.5,
B_MAX = 0.5;
function clamp01(x: number) { return Math.max(0, Math.min(1, x)); }
function norm(x: number, lo: number, hi: number) { return clamp01((x - lo) / (hi - lo)); }
function clamp01(x: number) {
return Math.max(0, Math.min(1, x));
}
function norm(x: number, lo: number, hi: number) {
return clamp01((x - lo) / (hi - lo));
}
function hilbertIndex15(x01: number, y01: number): number {
let x = Math.floor(clamp01(x01) * 32767);
let y = Math.floor(clamp01(y01) * 32767);
let index = 0;
for (let s = 1 << 14; s > 0; s >>= 1) {
const rx = (x & s) ? 1 : 0;
const ry = (y & s) ? 1 : 0;
const rx = x & s ? 1 : 0;
const ry = y & s ? 1 : 0;
index += s * s * ((3 * rx) ^ ry);
if (ry === 0) {
if (rx === 1) { x = 32767 - x; y = 32767 - y; }
const t = x; x = y; y = t;
if (rx === 1) {
x = 32767 - x;
y = 32767 - y;
}
const t = x;
x = y;
y = t;
}
}
return index >>> 0;
}
function centroidFromPaletteHexes(hexByType: Record<string, string | undefined>) {
function centroidFromPaletteHexes(
hexByType: Record<string, string | undefined>,
) {
const weights: Record<string, number> = {
Vibrant: 0.7,
Muted: 0.15,
@ -48,14 +61,20 @@ function centroidFromPaletteHexes(hexByType: Record<string, string | undefined>)
hexByType["LightVibrant"] ||
hexByType["LightMuted"];
let L = 0, A = 0, B = 0, W = 0;
let L = 0,
A = 0,
B = 0,
W = 0;
for (const [type, w] of Object.entries(weights)) {
const hex = hexByType[type] ?? fallbackHex;
if (!hex || w <= 0) continue;
const c = toOklab(parse(hex));
if (!c) continue;
L += c.l * w; A += c.a * w; B += c.b * w; W += w;
L += c.l * w;
A += c.a * w;
B += c.b * w;
W += w;
}
if (W === 0) return { l: 0.5, a: 0, b: 0 };
@ -78,7 +97,11 @@ export async function generateArtworkColorsForArtwork(artworkId: string) {
where: { id: artworkId },
select: {
file: { select: { fileKey: true } },
variants: { where: { type: "original" }, select: { s3Key: true }, take: 1 },
variants: {
where: { type: "original" },
select: { s3Key: true },
take: 1,
},
},
});
if (!artwork) throw new Error("Artwork not found");
@ -117,11 +140,14 @@ export async function generateArtworkColorsForArtwork(artworkId: string) {
}
const hexByType: Record<string, string | undefined> = Object.fromEntries(
vibrantHexes.map(({ type, hex }) => [type, hex])
vibrantHexes.map(({ type, hex }) => [type, hex]),
);
const { l, a, b } = centroidFromPaletteHexes(hexByType);
const sortKey = hilbertIndex15(norm(a, A_MIN, A_MAX), norm(b, B_MIN, B_MAX));
const sortKey = hilbertIndex15(
norm(a, A_MIN, A_MAX),
norm(b, B_MIN, B_MAX),
);
await prisma.artwork.update({
where: { id: artworkId },
@ -144,6 +170,9 @@ export async function generateArtworkColorsForArtwork(artworkId: string) {
colorError: e instanceof Error ? e.message : "Color generation failed",
},
});
return { ok: false as const, error: e instanceof Error ? e.message : "Color generation failed" };
return {
ok: false as const,
error: e instanceof Error ? e.message : "Color generation failed",
};
}
}

View File

@ -0,0 +1,130 @@
"use server";
import { prisma } from "@/lib/prisma";
import { s3 } from "@/lib/s3";
import { getImageBufferFromS3Key } from "@/utils/getImageBufferFromS3";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import sharp from "sharp";
const GALLERY_TARGET_SIZE = 300;
export async function generateGalleryVariant(
artworkId: string,
opts?: { force?: boolean },
) {
const artwork = await prisma.artwork.findUnique({
where: { id: artworkId },
include: { file: true, variants: true },
});
if (!artwork || !artwork.file) {
throw new Error("Artwork or file not found");
}
const existing = artwork.variants.find((v) => v.type === "gallery");
if (existing && !opts?.force) {
return { ok: true, skipped: true, variantId: existing.id };
}
const source =
artwork.variants.find((v) => v.type === "modified") ??
artwork.variants.find((v) => v.type === "original");
if (!source?.s3Key) {
throw new Error("Missing source variant");
}
const buffer = await getImageBufferFromS3Key(source.s3Key);
const srcMeta = await sharp(buffer).metadata();
const { width, height } = srcMeta;
let resizeOptions: { width?: number; height?: number };
if (width && height) {
resizeOptions =
height < width
? { height: GALLERY_TARGET_SIZE }
: { width: GALLERY_TARGET_SIZE };
} else {
resizeOptions = { height: GALLERY_TARGET_SIZE };
}
const galleryBuffer = await sharp(buffer)
.resize({ ...resizeOptions, withoutEnlargement: true })
.toFormat("webp")
.toBuffer();
const galleryMetadata = await sharp(galleryBuffer).metadata();
const galleryKey = `gallery/${artwork.file.fileKey}.webp`;
await s3.send(
new PutObjectCommand({
Bucket: `${process.env.BUCKET_NAME}`,
Key: galleryKey,
Body: galleryBuffer,
ContentType: "image/" + galleryMetadata.format,
}),
);
const variant = await prisma.fileVariant.upsert({
where: { artworkId_type: { artworkId: artwork.id, type: "gallery" } },
create: {
s3Key: galleryKey,
type: "gallery",
height: galleryMetadata.height ?? 0,
width: galleryMetadata.width ?? 0,
fileExtension: galleryMetadata.format,
mimeType: "image/" + galleryMetadata.format,
sizeBytes: galleryMetadata.size,
artworkId: artwork.id,
},
update: {
s3Key: galleryKey,
height: galleryMetadata.height ?? 0,
width: galleryMetadata.width ?? 0,
fileExtension: galleryMetadata.format,
mimeType: "image/" + galleryMetadata.format,
sizeBytes: galleryMetadata.size,
},
});
return { ok: true, skipped: false, variantId: variant.id };
}
export async function generateGalleryVariantsMissing(args?: {
limit?: number;
}) {
const limit = Math.min(Math.max(args?.limit ?? 20, 1), 100);
const artworks = await prisma.artwork.findMany({
where: { variants: { none: { type: "gallery" } } },
orderBy: [{ updatedAt: "asc" }, { id: "asc" }],
take: limit,
select: { id: true },
});
const results: Array<{ artworkId: string; ok: boolean; error?: string }> = [];
for (const a of artworks) {
try {
await generateGalleryVariant(a.id);
results.push({ artworkId: a.id, ok: true });
} catch (err) {
results.push({
artworkId: a.id,
ok: false,
error: err instanceof Error ? err.message : "Failed",
});
}
}
const ok = results.filter((r) => r.ok).length;
const failed = results.length - ok;
return {
picked: artworks.length,
processed: results.length,
ok,
failed,
results,
};
}

View File

@ -16,7 +16,8 @@ export async function getSingleArtwork(id: string) {
colors: { include: { color: true } },
// sortContexts: true,
tags: true,
variants: true
variants: true,
timelapse: true
}
})
}
}

View File

@ -0,0 +1,24 @@
"use server";
import { prisma } from "@/lib/prisma";
export type GalleryVariantStats = {
total: number;
withGallery: number;
missing: number;
};
export async function getGalleryVariantStats(): Promise<GalleryVariantStats> {
const [total, withGallery] = await Promise.all([
prisma.artwork.count(),
prisma.artwork.count({
where: { variants: { some: { type: "gallery" } } },
}),
]);
return {
total,
withGallery,
missing: total - withGallery,
};
}

View File

@ -0,0 +1,136 @@
"use server";
import { prisma } from "@/lib/prisma";
import { s3 } from "@/lib/s3";
import { PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { revalidatePath } from "next/cache";
import { z } from "zod/v4";
import { v4 as uuidv4 } from "uuid";
const createUploadSchema = z.object({
artworkId: z.string().min(1),
fileName: z.string().min(1),
mimeType: z.string().min(1),
sizeBytes: z.number().int().positive(),
});
/**
* Creates a presigned PUT url so the client can upload large timelapse videos directly to S3
* (avoids Next.js body-size/proxy limits).
*/
export async function createArtworkTimelapseUpload(input: z.infer<typeof createUploadSchema>) {
const { artworkId, fileName, mimeType, sizeBytes } = createUploadSchema.parse(input);
const ext = fileName.includes(".") ? fileName.split(".").pop() : undefined;
const suffix = ext ? `.${ext}` : "";
// Keep previous uploads unique so caching/CDNs won't bite you.
const fileId = uuidv4();
const s3Key = `timelapse/${artworkId}/${fileId}${suffix}`;
const cmd = new PutObjectCommand({
Bucket: `${process.env.BUCKET_NAME}`,
Key: s3Key,
ContentType: mimeType,
// If you want size enforcement at S3 level, you'd do that via policy; presigned PUT doesn't strictly enforce.
});
const uploadUrl = await getSignedUrl(s3, cmd, { expiresIn: 60 * 5 });
return { uploadUrl, s3Key, fileName, mimeType, sizeBytes };
}
const confirmSchema = z.object({
artworkId: z.string().min(1),
s3Key: z.string().min(1),
fileName: z.string().min(1),
mimeType: z.string().min(1),
sizeBytes: z.number().int().positive(),
});
/** Persist uploaded timelapse metadata in DB (upsert by artworkId). */
export async function confirmArtworkTimelapseUpload(input: z.infer<typeof confirmSchema>) {
const { artworkId, s3Key, fileName, mimeType, sizeBytes } = confirmSchema.parse(input);
// If an old timelapse exists, delete the old object so you don't leak storage.
const existing = await prisma.artworkTimelapse.findUnique({ where: { artworkId } });
if (existing?.s3Key && existing.s3Key !== s3Key) {
try {
await s3.send(
new DeleteObjectCommand({
Bucket: `${process.env.BUCKET_NAME}`,
Key: existing.s3Key,
})
);
} catch (err) {
// don't fail the request if cleanup fails
console.warn("Failed to delete previous timelapse object", existing.s3Key, err);
}
}
await prisma.artworkTimelapse.upsert({
where: { artworkId },
create: {
artworkId,
s3Key,
fileName,
mimeType,
sizeBytes,
enabled: true,
},
update: {
s3Key,
fileName,
mimeType,
sizeBytes,
},
});
revalidatePath(`/artworks/${artworkId}`);
return { ok: true };
}
const enabledSchema = z.object({
artworkId: z.string().min(1),
enabled: z.boolean(),
});
export async function setArtworkTimelapseEnabled(input: z.infer<typeof enabledSchema>) {
const { artworkId, enabled } = enabledSchema.parse(input);
await prisma.artworkTimelapse.update({
where: { artworkId },
data: { enabled },
});
revalidatePath(`/artworks/${artworkId}`);
return { ok: true };
}
const deleteSchema = z.object({
artworkId: z.string().min(1),
});
export async function deleteArtworkTimelapse(input: z.infer<typeof deleteSchema>) {
const { artworkId } = deleteSchema.parse(input);
const existing = await prisma.artworkTimelapse.findUnique({ where: { artworkId } });
if (!existing) return { ok: true };
try {
await s3.send(
new DeleteObjectCommand({
Bucket: `${process.env.BUCKET_NAME}`,
Key: existing.s3Key,
})
);
} catch (err) {
console.warn("Failed to delete timelapse object", existing.s3Key, err);
}
await prisma.artworkTimelapse.delete({ where: { artworkId } });
revalidatePath(`/artworks/${artworkId}`);
return { ok: true };
}

View File

@ -94,4 +94,4 @@ export async function updateArtwork(
});
return updatedArtwork;
}
}

View File

@ -10,7 +10,7 @@ export async function deleteCategory(catId: string) {
id: true,
_count: {
select: {
tags: true,
tagLinks: true,
artworks: true
},
},
@ -25,7 +25,7 @@ export async function deleteCategory(catId: string) {
throw new Error("Cannot delete category: it is used by artworks.");
}
if (cat._count.tags > 0) {
if (cat._count.tagLinks > 0) {
throw new Error("Cannot delete category: it is used by tags.");
}
@ -34,4 +34,4 @@ export async function deleteCategory(catId: string) {
revalidatePath("/categories");
return { success: true };
}
}

View File

@ -3,14 +3,17 @@
import { prisma } from "@/lib/prisma"
export async function getCategoriesWithTags() {
return await prisma.artCategory.findMany({ include: { tags: true }, orderBy: { sortIndex: "asc" } })
return await prisma.artCategory.findMany({
include: { tagLinks: { include: { tag: true } } },
orderBy: { sortIndex: "asc" },
})
}
export async function getCategoriesWithCount() {
return await prisma.artCategory.findMany({
include: {
_count: { select: { artworks: true, tags: true } },
_count: { select: { artworks: true, tagLinks: true } },
},
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})
}
}

View File

@ -1,6 +1,6 @@
"use server";
import { Prisma } from "@/generated/prisma/client";
import type { Prisma } from "@/generated/prisma/client";
import { prisma } from "@/lib/prisma";
import { generateArtworkColorsForArtwork } from "../artworks/generateArtworkColors";

View File

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

View File

@ -0,0 +1,94 @@
"use server";
import { s3 } from "@/lib/s3";
import {
DeleteObjectCommand,
ListObjectsV2Command,
PutObjectCommand,
} from "@aws-sdk/client-s3";
const PREFIX = "commissions/custom-cards/";
export type CommissionCustomCardImageItem = {
key: string;
url: string;
size: number | null;
lastModified: string | null;
};
function buildImageUrl(key: string) {
return `/api/image/${encodeURI(key)}`;
}
function sanitizeFilename(name: string) {
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
}
export async function listCommissionCustomCardImages(): Promise<
CommissionCustomCardImageItem[]
> {
const command = new ListObjectsV2Command({
Bucket: `${process.env.BUCKET_NAME}`,
Prefix: PREFIX,
});
const res = await s3.send(command);
return (
res.Contents?.filter((obj) => obj.Key && obj.Key !== PREFIX).map((obj) => {
const key = obj.Key as string;
return {
key,
url: buildImageUrl(key),
size: obj.Size ?? null,
lastModified: obj.LastModified?.toISOString() ?? null,
};
}) ?? []
);
}
export async function uploadCommissionCustomCardImage(
formData: FormData
): Promise<CommissionCustomCardImageItem> {
const file = formData.get("file");
if (!(file instanceof File)) {
throw new Error("Missing file");
}
if (!file.type.startsWith("image/")) {
throw new Error("Only image uploads are allowed");
}
const safeName = sanitizeFilename(file.name || "custom-card");
const key = `${PREFIX}${Date.now()}-${safeName}`;
const buffer = Buffer.from(await file.arrayBuffer());
await s3.send(
new PutObjectCommand({
Bucket: `${process.env.BUCKET_NAME}`,
Key: key,
Body: buffer,
ContentType: file.type,
})
);
return {
key,
url: buildImageUrl(key),
size: file.size,
lastModified: new Date().toISOString(),
};
}
export async function deleteCommissionCustomCardImage(key: string) {
if (!key.startsWith(PREFIX)) {
throw new Error("Invalid key");
}
await s3.send(
new DeleteObjectCommand({
Bucket: `${process.env.BUCKET_NAME}`,
Key: key,
})
);
}

View File

@ -0,0 +1,55 @@
"use server";
import { prisma } from "@/lib/prisma";
import {
commissionCustomCardSchema,
type CommissionCustomCardValues,
} from "@/schemas/commissionCustomCard";
export async function createCommissionCustomCard(
formData: CommissionCustomCardValues
) {
const parsed = commissionCustomCardSchema.safeParse(formData);
if (!parsed.success) {
console.error("Validation failed", parsed.error);
throw new Error("Invalid input");
}
const data = parsed.data;
const created = await prisma.commissionCustomCard.create({
data: {
name: data.name,
description: data.description,
referenceImageUrl: data.referenceImageUrl ?? null,
isVisible: data.isVisible ?? true,
isSpecialOffer: data.isSpecialOffer ?? false,
tags: data.tagIds?.length
? { connect: data.tagIds.map((id) => ({ id })) }
: undefined,
options: {
create:
data.options?.map((opt, index) => ({
option: { connect: { id: opt.optionId } },
price: opt.price,
pricePercent: opt.pricePercent,
priceRange: opt.priceRange,
sortIndex: index,
})) ?? [],
},
extras: {
create:
data.extras?.map((ext, index) => ({
extra: { connect: { id: ext.extraId } },
price: ext.price,
pricePercent: ext.pricePercent,
priceRange: ext.priceRange,
sortIndex: index,
})) ?? [],
},
},
});
return created;
}

View File

@ -0,0 +1,57 @@
"use server";
import { prisma } from "@/lib/prisma";
import {
commissionCustomCardSchema,
type CommissionCustomCardValues,
} from "@/schemas/commissionCustomCard";
export async function updateCommissionCustomCard(
id: string,
rawData: CommissionCustomCardValues
) {
const data = commissionCustomCardSchema.parse(rawData);
const updated = await prisma.commissionCustomCard.update({
where: { id },
data: {
name: data.name,
description: data.description,
referenceImageUrl: data.referenceImageUrl ?? null,
isVisible: data.isVisible ?? true,
isSpecialOffer: data.isSpecialOffer ?? false,
tags: data.tagIds
? {
set: [],
connect: data.tagIds.map((id) => ({ id })),
}
: undefined,
options: {
deleteMany: {},
create: data.options?.map((opt, index) => ({
option: { connect: { id: opt.optionId } },
price: opt.price ?? null,
pricePercent: opt.pricePercent ?? null,
priceRange: opt.priceRange ?? null,
sortIndex: index,
})),
},
extras: {
deleteMany: {},
create: data.extras?.map((ext, index) => ({
extra: { connect: { id: ext.extraId } },
price: ext.price ?? null,
pricePercent: ext.pricePercent ?? null,
priceRange: ext.priceRange ?? null,
sortIndex: index,
})),
},
},
include: {
options: true,
extras: true,
},
});
return updated;
}

View File

@ -0,0 +1,16 @@
"use server";
import { prisma } from "@/lib/prisma";
export async function updateCommissionCustomCardSortOrder(
items: { id: string; sortIndex: number }[]
) {
await prisma.$transaction(
items.map((item) =>
prisma.commissionCustomCard.update({
where: { id: item.id },
data: { sortIndex: item.sortIndex },
})
)
);
}

View File

@ -0,0 +1,92 @@
"use server";
import { s3 } from "@/lib/s3";
import {
DeleteObjectCommand,
ListObjectsV2Command,
PutObjectCommand,
} from "@aws-sdk/client-s3";
const PREFIX = "commissions/examples/";
export type CommissionExampleItem = {
key: string;
url: string;
size: number | null;
lastModified: string | null;
};
function buildImageUrl(key: string) {
return `/api/image/${encodeURI(key)}`;
}
function sanitizeFilename(name: string) {
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
}
export async function listCommissionExamples(): Promise<CommissionExampleItem[]> {
const command = new ListObjectsV2Command({
Bucket: `${process.env.BUCKET_NAME}`,
Prefix: PREFIX,
});
const res = await s3.send(command);
return (
res.Contents?.filter((obj) => obj.Key && obj.Key !== PREFIX).map((obj) => {
const key = obj.Key as string;
return {
key,
url: buildImageUrl(key),
size: obj.Size ?? null,
lastModified: obj.LastModified?.toISOString() ?? null,
};
}) ?? []
);
}
export async function uploadCommissionExample(
formData: FormData
): Promise<CommissionExampleItem> {
const file = formData.get("file");
if (!(file instanceof File)) {
throw new Error("Missing file");
}
if (!file.type.startsWith("image/")) {
throw new Error("Only image uploads are allowed");
}
const safeName = sanitizeFilename(file.name || "example");
const key = `${PREFIX}${Date.now()}-${safeName}`;
const buffer = Buffer.from(await file.arrayBuffer());
await s3.send(
new PutObjectCommand({
Bucket: `${process.env.BUCKET_NAME}`,
Key: key,
Body: buffer,
ContentType: file.type,
})
);
return {
key,
url: buildImageUrl(key),
size: file.size,
lastModified: new Date().toISOString(),
};
}
export async function deleteCommissionExample(key: string) {
if (!key.startsWith(PREFIX)) {
throw new Error("Invalid key");
}
await s3.send(
new DeleteObjectCommand({
Bucket: `${process.env.BUCKET_NAME}`,
Key: key,
})
);
}

View File

@ -2,10 +2,21 @@
import { prisma } from "@/lib/prisma";
export async function getActiveGuidelines(): Promise<string | null> {
export async function getActiveGuidelines(): Promise<{
markdown: string | null;
exampleImageUrl: string | null;
}> {
const guidelines = await prisma.commissionGuidelines.findFirst({
where: { isActive: true },
orderBy: { createdAt: 'desc' },
orderBy: { createdAt: "desc" },
select: {
markdown: true,
exampleImageUrl: true,
},
});
return guidelines?.markdown ?? null;
return {
markdown: guidelines?.markdown ?? null,
exampleImageUrl: guidelines?.exampleImageUrl ?? null,
};
}

View File

@ -2,7 +2,7 @@
import { prisma } from "@/lib/prisma";
export async function saveGuidelines(markdown: string) {
export async function saveGuidelines(markdown: string, exampleImageUrl: string | null) {
await prisma.commissionGuidelines.updateMany({
where: { isActive: true },
data: { isActive: false },
@ -11,6 +11,7 @@ export async function saveGuidelines(markdown: string) {
await prisma.commissionGuidelines.create({
data: {
markdown,
exampleImageUrl: exampleImageUrl || null,
},
});
}
}

View File

@ -0,0 +1,27 @@
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { COMMISSION_STATUSES } from "@/lib/commissions/kanban";
import { prisma } from "@/lib/prisma"; // adjust to your prisma import
// import { requireAdmin } from "@/lib/auth/requireAdmin"; // recommended if you have it
const schema = z.object({
id: z.string().min(1),
status: z.enum(COMMISSION_STATUSES),
});
export async function updateCommissionRequestStatus(input: z.infer<typeof schema>) {
// await requireAdmin(); // enforce auth/role check here
const { id, status } = schema.parse(input);
await prisma.commissionRequest.update({
where: { id },
data: { status },
});
// revalidate the board page so a refresh always reflects server truth
revalidatePath("/commissions/board");
}

View File

@ -0,0 +1,30 @@
"use server";
import { prisma } from "@/lib/prisma";
import { commissionExtraSchema } from "@/schemas/commissionType";
import { revalidatePath } from "next/cache";
const LIST_PATH = "/commissions/extras";
export async function createCommissionExtra(input: unknown) {
const data = commissionExtraSchema.parse(input);
const created = await prisma.commissionExtra.create({ data });
revalidatePath(LIST_PATH);
return created;
}
export async function updateCommissionExtra(id: string, input: unknown) {
const data = commissionExtraSchema.parse(input);
const updated = await prisma.commissionExtra.update({ where: { id }, data });
revalidatePath(LIST_PATH);
return updated;
}
export async function deleteCommissionExtra(id: string) {
// Optional safety:
// const used = await prisma.commissionTypeExtra.count({ where: { extraId: id } });
// if (used > 0) throw new Error("Extra is linked to types.");
console.log("TBD");
// await prisma.commissionExtra.delete({ where: { id } });
// revalidatePath(LIST_PATH);
}

View File

@ -1,7 +1,7 @@
"use server"
"use server";
import { prisma } from "@/lib/prisma"
import { commissionTypeSchema } from "@/schemas/commissionType"
import { prisma } from "@/lib/prisma";
import { commissionTypeSchema } from "@/schemas/commissionType";
export async function createCommissionOption(data: { name: string }) {
return await prisma.commissionOption.create({
@ -9,7 +9,7 @@ export async function createCommissionOption(data: { name: string }) {
name: data.name,
description: "",
},
})
});
}
export async function createCommissionExtra(data: { name: string }) {
@ -18,64 +18,70 @@ export async function createCommissionExtra(data: { name: string }) {
name: data.name,
description: "",
},
})
});
}
export async function createCommissionCustomInput(data: {
name: string
fieldId: string
name: string;
fieldId: string;
}) {
return await prisma.commissionCustomInput.create({
data: {
name: data.name,
fieldId: data.fieldId,
},
})
});
}
export async function createCommissionType(formData: commissionTypeSchema) {
const parsed = commissionTypeSchema.safeParse(formData)
const parsed = commissionTypeSchema.safeParse(formData);
if (!parsed.success) {
console.error("Validation failed", parsed.error)
throw new Error("Invalid input")
console.error("Validation failed", parsed.error);
throw new Error("Invalid input");
}
const data = parsed.data
const data = parsed.data;
const created = await prisma.commissionType.create({
data: {
name: data.name,
description: data.description,
tags: data.tagIds?.length
? { connect: data.tagIds.map((id) => ({ id })) }
: undefined,
options: {
create: data.options?.map((opt, index) => ({
option: { connect: { id: opt.optionId } },
price: opt.price,
pricePercent: opt.pricePercent,
priceRange: opt.priceRange,
sortIndex: index,
})) || [],
create:
data.options?.map((opt, index) => ({
option: { connect: { id: opt.optionId } },
price: opt.price,
pricePercent: opt.pricePercent,
priceRange: opt.priceRange,
sortIndex: index,
})) || [],
},
extras: {
create: data.extras?.map((ext, index) => ({
extra: { connect: { id: ext.extraId } },
price: ext.price,
pricePercent: ext.pricePercent,
priceRange: ext.priceRange,
sortIndex: index,
})) || [],
create:
data.extras?.map((ext, index) => ({
extra: { connect: { id: ext.extraId } },
price: ext.price,
pricePercent: ext.pricePercent,
priceRange: ext.priceRange,
sortIndex: index,
})) || [],
},
customInputs: {
create: data.customInputs?.map((c, index) => ({
customInput: { connect: { id: c.customInputId } },
label: c.label,
inputType: c.inputType,
required: c.required,
sortIndex: index,
})) || [],
create:
data.customInputs?.map((c, index) => ({
customInput: { connect: { id: c.customInputId } },
label: c.label,
inputType: c.inputType,
required: c.required,
sortIndex: index,
})) || [],
},
},
})
});
return created
}
return created;
}

View File

@ -0,0 +1,43 @@
"use server";
import { prisma } from "@/lib/prisma";
import { commissionOptionSchema } from "@/schemas/commissionType";
import { revalidatePath } from "next/cache";
const LIST_PATH = "/commissions/options";
function toInt(v: string) {
const n = Number.parseInt(v, 10);
return Number.isFinite(n) ? n : 0;
}
export async function createCommissionOption(input: unknown) {
const data = commissionOptionSchema.parse(input);
const created = await prisma.commissionOption.create({
data: {
name: data.name,
description: data.description?.trim() ? data.description : null,
},
});
revalidatePath(LIST_PATH);
return created;
}
export async function updateCommissionOption(id: string, input: unknown) {
const data = commissionOptionSchema.parse(input);
const updated = await prisma.commissionOption.update({
where: { id },
data: {
name: data.name,
description: data.description?.trim() ? data.description : null,
},
});
revalidatePath(LIST_PATH);
return updated;
}
export async function deleteCommissionOption(id: string) {
console.log("TBD");
// await prisma.commissionOption.delete({ where: { id } });
// revalidatePath(LIST_PATH);
}

View File

@ -1,20 +1,26 @@
"use server"
"use server";
import { prisma } from "@/lib/prisma"
import { commissionTypeSchema } from "@/schemas/commissionType"
import * as z from "zod/v4"
import { prisma } from "@/lib/prisma";
import { commissionTypeSchema } from "@/schemas/commissionType";
import type * as z from "zod/v4";
export async function updateCommissionType(
id: string,
rawData: z.infer<typeof commissionTypeSchema>
rawData: z.infer<typeof commissionTypeSchema>,
) {
const data = commissionTypeSchema.parse(rawData)
const data = commissionTypeSchema.parse(rawData);
const updated = await prisma.commissionType.update({
where: { id },
data: {
name: data.name,
description: data.description,
tags: data.tagIds
? {
set: [],
connect: data.tagIds.map((id) => ({ id })),
}
: undefined,
options: {
deleteMany: {},
create: data.options?.map((opt, index) => ({
@ -37,13 +43,14 @@ export async function updateCommissionType(
},
customInputs: {
deleteMany: {},
create: data.customInputs?.map((c, index) => ({
customInput: { connect: { id: c.customInputId } },
label: c.label,
inputType: c.inputType,
required: c.required,
sortIndex: index,
})) || [],
create:
data.customInputs?.map((c, index) => ({
customInput: { connect: { id: c.customInputId } },
label: c.label,
inputType: c.inputType,
required: c.required,
sortIndex: index,
})) || [],
},
},
include: {
@ -51,7 +58,7 @@ export async function updateCommissionType(
extras: true,
customInputs: true,
},
})
});
return updated
return updated;
}

View File

@ -17,30 +17,30 @@ export async function createTag(formData: TagFormInput) {
const tagSlug = data.name.toLowerCase().replace(/\s+/g, "-");
const created = await prisma.$transaction(async (tx) => {
const tag = await tx.artTag.create({
const tag = await tx.tag.create({
data: {
name: data.name,
slug: tagSlug,
description: data.description,
isParent: data.isParent,
showOnAnimalPage: data.showOnAnimalPage,
parentId
isVisible: data.isVisible ?? true,
},
});
if (data.categoryIds) {
await tx.artTag.update({
where: { id: tag.id },
data: {
categories: {
set: data.categoryIds.map(id => ({ id }))
}
}
await tx.tagCategory.createMany({
data: data.categoryIds.map((categoryId) => ({
tagId: tag.id,
categoryId,
isParent: data.isParent,
showOnAnimalPage: data.showOnAnimalPage,
parentTagId: parentId,
})),
skipDuplicates: true,
});
}
if (data.aliases && data.aliases.length > 0) {
await tx.artTagAlias.createMany({
await tx.tagAlias.createMany({
data: data.aliases.map((alias) => ({
tagId: tag.id,
alias,
@ -53,4 +53,4 @@ export async function createTag(formData: TagFormInput) {
});
return created
}
}

View File

@ -4,14 +4,13 @@ import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function deleteTag(tagId: string) {
const tag = await prisma.artTag.findUnique({
const tag = await prisma.tag.findUnique({
where: { id: tagId },
select: {
id: true,
_count: {
select: {
artworks: true,
children: true,
},
},
},
@ -25,16 +24,21 @@ export async function deleteTag(tagId: string) {
throw new Error("Cannot delete tag: it is used by artworks.");
}
if (tag._count.children > 0) {
const parentUsage = await prisma.tagCategory.count({
where: { parentTagId: tagId },
});
if (parentUsage > 0) {
throw new Error("Cannot delete tag: it has child tags.");
}
await prisma.$transaction(async (tx) => {
await tx.artTagAlias.deleteMany({ where: { tagId } });
await tx.artTag.delete({ where: { id: tagId } });
await tx.tagAlias.deleteMany({ where: { tagId } });
await tx.tagCategory.deleteMany({ where: { tagId } });
await tx.tag.delete({ where: { id: tagId } });
});
revalidatePath("/tags");
return { success: true };
}
}

View File

@ -3,5 +3,5 @@
import { prisma } from "@/lib/prisma"
export async function getTags() {
return await prisma.artTag.findMany({ orderBy: { sortIndex: "asc" } })
}
return await prisma.tag.findMany({ orderBy: { sortIndex: "asc" } })
}

View File

@ -3,20 +3,26 @@
import { prisma } from "@/lib/prisma";
export async function isDescendant(tagId: string, possibleAncestorId: string): Promise<boolean> {
// Walk upwards from possibleAncestorId; if we hit tagId, it's a cycle.
let current: string | null = possibleAncestorId;
while (current) {
// Walk upwards across any category hierarchy; if we hit tagId, it's a cycle.
const visited = new Set<string>();
const queue: string[] = [possibleAncestorId];
while (queue.length > 0) {
const current = queue.shift();
if (!current) continue;
if (current === tagId) return true;
if (visited.has(current)) continue;
visited.add(current);
const t: { parentId: string | null } | null =
await prisma.artTag.findUnique({
where: { id: current },
select: { parentId: true },
});
const parents = await prisma.tagCategory.findMany({
where: { tagId: current, parentTagId: { not: null } },
select: { parentTagId: true },
});
current = t?.parentId ?? null;
for (const p of parents) {
if (p.parentTagId) queue.push(p.parentTagId);
}
}
return false;
}
}

View File

@ -27,22 +27,62 @@ export async function updateTag(id: string, rawData: TagFormInput) {
}
const updated = await prisma.$transaction(async (tx) => {
const tag = await tx.artTag.update({
const tag = await tx.tag.update({
where: { id },
data: {
name: data.name,
slug: tagSlug,
description: data.description,
isParent: data.isParent,
showOnAnimalPage: data.showOnAnimalPage,
parentId,
categories: data.categoryIds
? { set: data.categoryIds.map((cid) => ({ id: cid })) }
: undefined,
isVisible: data.isVisible ?? true,
},
});
const existing = await tx.artTagAlias.findMany({
if (data.categoryIds) {
const existingLinks = await tx.tagCategory.findMany({
where: { tagId: id },
select: { id: true, categoryId: true },
});
const desired = new Set(data.categoryIds);
const existingSet = new Set(existingLinks.map((l) => l.categoryId));
const toCreate = data.categoryIds.filter((cid) => !existingSet.has(cid));
const toDeleteIds = existingLinks
.filter((l) => !desired.has(l.categoryId))
.map((l) => l.id);
if (toDeleteIds.length > 0) {
await tx.tagCategory.deleteMany({
where: { id: { in: toDeleteIds } },
});
}
if (toCreate.length > 0) {
await tx.tagCategory.createMany({
data: toCreate.map((categoryId) => ({
tagId: id,
categoryId,
isParent: data.isParent,
showOnAnimalPage: data.showOnAnimalPage,
parentTagId: parentId,
})),
skipDuplicates: true,
});
}
if (existingLinks.length > 0) {
await tx.tagCategory.updateMany({
where: { tagId: id, categoryId: { in: data.categoryIds } },
data: {
isParent: data.isParent,
showOnAnimalPage: data.showOnAnimalPage,
parentTagId: parentId,
},
});
}
}
const existing = await tx.tagAlias.findMany({
where: { tagId: id },
select: { id: true, alias: true },
});
@ -56,13 +96,13 @@ export async function updateTag(id: string, rawData: TagFormInput) {
.map((a) => a.id);
if (toDeleteIds.length > 0) {
await tx.artTagAlias.deleteMany({
await tx.tagAlias.deleteMany({
where: { id: { in: toDeleteIds } },
});
}
if (toCreate.length > 0) {
await tx.artTagAlias.createMany({
await tx.tagAlias.createMany({
data: toCreate.map((alias) => ({ tagId: id, alias })),
skipDuplicates: true,
});
@ -72,4 +112,4 @@ export async function updateTag(id: string, rawData: TagFormInput) {
});
return updated
}
}

View File

@ -8,7 +8,10 @@ import sharp from "sharp";
import { v4 as uuidv4 } from "uuid";
import { generateArtworkColorsForArtwork } from "../artworks/generateArtworkColors";
export async function createImageFromFile(imageFile: File, opts?: { originalName?: string, colorMode?: "inline" | "defer" | "off" }) {
export async function createImageFromFile(
imageFile: File,
opts?: { originalName?: string; colorMode?: "inline" | "defer" | "off" },
) {
if (!(imageFile instanceof File)) {
console.log("No image or invalid type");
return null;
@ -29,6 +32,7 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
const modifiedKey = `modified/${fileKey}.webp`;
const resizedKey = `resized/${fileKey}.webp`;
const thumbnailKey = `thumbnail/${fileKey}.webp`;
const galleryKey = `gallery/${fileKey}.webp`;
const sharpData = sharp(buffer);
const metadata = await sharpData.metadata();
@ -40,7 +44,7 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
Key: originalKey,
Body: buffer,
ContentType: "image/" + metadata.format,
})
}),
);
//--- Modified file
@ -53,7 +57,7 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
Key: modifiedKey,
Body: modifiedBuffer,
ContentType: "image/" + modifiedMetadata.format,
})
}),
);
//--- Resized file
@ -62,7 +66,8 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
let resizeOptions: { width?: number; height?: number };
if (width && height) {
resizeOptions = height < width ? { height: targetSize } : { width: targetSize };
resizeOptions =
height < width ? { height: targetSize } : { width: targetSize };
} else {
resizeOptions = { height: targetSize };
}
@ -80,7 +85,7 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
Key: resizedKey,
Body: resizedBuffer,
ContentType: "image/" + resizedMetadata.format,
})
}),
);
//--- Thumbnail file
@ -88,7 +93,10 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
let thumbnailOptions: { width?: number; height?: number };
if (width && height) {
thumbnailOptions = height < width ? { height: thumbnailTargetSize } : { width: thumbnailTargetSize };
thumbnailOptions =
height < width
? { height: thumbnailTargetSize }
: { width: thumbnailTargetSize };
} else {
thumbnailOptions = { height: thumbnailTargetSize };
}
@ -106,7 +114,36 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
Key: thumbnailKey,
Body: thumbnailBuffer,
ContentType: "image/" + thumbnailMetadata.format,
})
}),
);
//--- Gallery file
const galleryTargetSize = 300;
let galleryOptions: { width?: number; height?: number };
if (width && height) {
galleryOptions =
height < width
? { height: galleryTargetSize }
: { width: galleryTargetSize };
} else {
galleryOptions = { height: galleryTargetSize };
}
const galleryBuffer = await sharp(modifiedBuffer)
.resize({ ...galleryOptions, withoutEnlargement: true })
.toFormat("webp")
.toBuffer();
const galleryMetadata = await sharp(galleryBuffer).metadata();
await s3.send(
new PutObjectCommand({
Bucket: `${process.env.BUCKET_NAME}`,
Key: galleryKey,
Body: galleryBuffer,
ContentType: "image/" + galleryMetadata.format,
}),
);
const fileRecord = await prisma.fileData.create({
@ -114,7 +151,7 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
name: fileName,
fileKey,
originalFile: fileName,
uploadDate: lastModified,
uploadDate: new Date(),
fileType: realFileType,
fileSize,
},
@ -193,6 +230,16 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
sizeBytes: thumbnailMetadata.size,
artworkId: artworkRecord.id,
},
{
s3Key: galleryKey,
type: "gallery",
height: galleryMetadata.height ?? 0,
width: galleryMetadata.width ?? 0,
fileExtension: galleryMetadata.format,
mimeType: "image/" + galleryMetadata.format,
sizeBytes: galleryMetadata.size,
artworkId: artworkRecord.id,
},
],
});
@ -206,6 +253,5 @@ export async function createImageFromFile(imageFile: File, opts?: { originalName
// (nothing else to do here)
}
return artworkRecord;
}
}

View File

@ -3,6 +3,7 @@ import { getCategoriesWithTags } from "@/actions/categories/getCategories";
import { getTags } from "@/actions/tags/getTags";
import ArtworkColors from "@/components/artworks/single/ArtworkColors";
import ArtworkDetails from "@/components/artworks/single/ArtworkDetails";
import ArtworkTimelapse from "@/components/artworks/single/ArtworkTimelapse";
import ArtworkVariants from "@/components/artworks/single/ArtworkVariants";
import DeleteArtworkButton from "@/components/artworks/single/DeleteArtworkButton";
import EditArtworkForm from "@/components/artworks/single/EditArtworkForm";
@ -21,13 +22,13 @@ export default async function ArtworkSinglePage({ params }: { params: Promise<{
<div>
<h1 className="text-2xl font-bold mb-4">Edit artwork</h1>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<div className="space-y-6">
{item ? <EditArtworkForm artwork={item} tags={tags} categories={categories} /> : 'Artwork not found...'}
<div className="mt-6">
<div>
{item && <DeleteArtworkButton artworkId={item.id} />}
</div>
<div>
{item && <ArtworkVariants variants={item.variants} />}
{item && <ArtworkTimelapse artworkId={item.id} timelapse={item.timelapse} />}
</div>
</div>
<div className="space-y-6">
@ -37,8 +38,11 @@ export default async function ArtworkSinglePage({ params }: { params: Promise<{
<div>
{item && <ArtworkDetails artwork={item} />}
</div>
<div>
{item && <ArtworkVariants artworkId={item.id} variants={item.variants} />}
</div>
</div>
</div>
</div>
);
}
}

View File

@ -1,92 +1,12 @@
import { ArtworkColorProcessor } from "@/components/artworks/ArtworkColorProcessor";
import { ArtworksTable } from "@/components/artworks/ArtworksTable";
import { getArtworksPage } from "@/lib/queryArtworks";
export default async function ArtworksPage({
searchParams
}: {
searchParams?: {
// type?: string;
published?: string;
// groupBy?: string;
// year?: string;
// album?: string;
cursor?: string;
}
}) {
const {
// type = "all",
published = "all",
// groupBy = "year",
// year,
// album,
cursor = undefined
} = await searchParams ?? {};
// const groupMode = groupBy === "album" ? "album" : "year";
// const groupId = groupMode === "album" ? album ?? "all" : year ?? "all";
// Filter by type
// if (type !== "all") {
// where.typeId = type === "none" ? null : type;
// }
// Filter by published status
// if (published === "published") {
// where.published = true;
// } else if (published === "unpublished") {
// where.published = false;
// } else if (published === "needsWork") {
// where.needsWork = true;
// }
// Filter by group (year or album)
// if (groupMode === "year" && groupId !== "all") {
// where.year = parseInt(groupId);
// } else if (groupMode === "album" && groupId !== "all") {
// where.albumId = groupId;
// }
const { items, nextCursor } = await getArtworksPage(
{
published,
cursor,
take: 48
}
)
export default async function ArtworksPage() {
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Artworks</h1>
{/* <ProcessArtworkColorsButton /> */}
<ArtworkColorProcessor />
<ArtworksTable />
</div>
// <div>
// <div className="flex justify-between pb-4 items-end">
// <h1 className="text-2xl font-bold mb-4">Artworks</h1>
// <Link href="/uploads/single" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
// <PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Upload new artwork
// </Link>
// <Link href="/uploads/bulk" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
// <PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Upload many artwork
// </Link>
// </div>
// <FilterBar
// // types={types}
// // albums={albums}
// // years={years}
// // currentType={type}
// currentPublished={published}
// // groupBy={groupMode}
// // groupId={groupId}
// />
// <div className="mt-6">
// {items.length > 0 ? (
// <ArtworkGallery initialArtworks={items} initialCursor={nextCursor} />
// ) : (
// <p className="text-muted-foreground italic">No artworks found.</p>
// )}
// </div>
// </div >
);
}
}

View File

@ -0,0 +1,46 @@
import { listCommissionCustomCardImages } from "@/actions/commissions/customCards/images";
import EditCustomCardForm from "@/components/commissions/customCards/EditCustomCardForm";
import { prisma } from "@/lib/prisma";
import { notFound } from "next/navigation";
export default async function CommissionCustomCardEditPage({
params,
}: {
params: { id: string };
}) {
const { id } = await params;
const [card, options, extras, images, tags] = await Promise.all([
prisma.commissionCustomCard.findUnique({
where: { id },
include: {
options: { orderBy: { sortIndex: "asc" } },
extras: { orderBy: { sortIndex: "asc" } },
tags: true,
},
}),
prisma.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
listCommissionCustomCardImages(),
prisma.tag.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
]);
if (!card) {
notFound();
}
return (
<div>
<div className="flex gap-4 justify-between pb-8">
<h1 className="text-2xl font-bold mb-4">Edit Custom Commission Card</h1>
</div>
<EditCustomCardForm
card={card}
allOptions={options}
allExtras={extras}
images={images}
allTags={tags}
/>
</div>
);
}

View File

@ -0,0 +1,21 @@
import { listCommissionCustomCardImages } from "@/actions/commissions/customCards/images";
import NewCustomCardForm from "@/components/commissions/customCards/NewCustomCardForm";
import { prisma } from "@/lib/prisma";
export default async function CommissionCustomCardsNewPage() {
const [options, extras, images, tags] = await Promise.all([
prisma.commissionOption.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
prisma.commissionExtra.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
listCommissionCustomCardImages(),
prisma.tag.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }] }),
]);
return (
<div>
<div className="flex gap-4 justify-between pb-8">
<h1 className="text-2xl font-bold mb-4">New Custom Commission Card</h1>
</div>
<NewCustomCardForm options={options} extras={extras} images={images} tags={tags} />
</div>
);
}

View File

@ -0,0 +1,34 @@
import ListCustomCards from "@/components/commissions/customCards/ListCustomCards";
import { prisma } from "@/lib/prisma";
import { PlusCircleIcon } from "lucide-react";
import Link from "next/link";
export default async function CommissionCustomCardsPage() {
const cards = await prisma.commissionCustomCard.findMany({
include: {
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
},
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
});
return (
<div>
<div className="flex gap-4 justify-between pb-8">
<h1 className="text-2xl font-bold mb-4">Custom Commission Cards</h1>
<Link
href="/commissions/custom-cards/new"
className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded"
>
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" />
Add new Card
</Link>
</div>
{cards && cards.length > 0 ? (
<ListCustomCards cards={cards} />
) : (
<p className="text-muted-foreground italic">No custom cards found.</p>
)}
</div>
);
}

View File

@ -1,8 +1,12 @@
import { listCommissionExamples } from "@/actions/commissions/examples";
import { getActiveGuidelines } from "@/actions/commissions/guidelines/getGuidelines";
import GuidelinesEditor from "@/components/commissions/guidelines/Editor";
export default async function CommissionGuidelinesPage() {
const markdown = await getActiveGuidelines();
const [{ markdown, exampleImageUrl }, examples] = await Promise.all([
getActiveGuidelines(),
listCommissionExamples(),
]);
return (
<div>
@ -10,8 +14,12 @@ export default async function CommissionGuidelinesPage() {
<h1 className="text-2xl font-bold mb-4">Commission Guidelines</h1>
</div>
<div className="space-y-4 p-1 border rounded-xl bg-muted/20">
<GuidelinesEditor markdown={markdown} />
<GuidelinesEditor
markdown={markdown}
exampleImageUrl={exampleImageUrl}
examples={examples}
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,47 @@
import CommissionsKanbanClient from "@/components/commissions/kanban/CommissionsKanbanClient";
import { columnIdForStatus } from "@/lib/commissions/kanban";
import { prisma } from "@/lib/prisma";
import type { BoardItem, ColumnsState } from "@/types/Board";
export default async function CommissionsBoardPage() {
const requests = await prisma.commissionRequest.findMany({
where: {
status: { in: ["NEW", "REVIEWING", "ACCEPTED", "INPROGRESS", "COMPLETED"] },
},
orderBy: [{ createdAt: "desc" }],
include: {
type: true,
option: true,
extras: true,
files: true,
},
});
const initial: ColumnsState = {
intake: [],
inProgress: [],
completed: [],
};
for (const r of requests) {
const col = columnIdForStatus(r.status) ?? "intake";
const item: BoardItem = {
id: r.id,
createdAt: r.createdAt.toISOString(),
status: r.status,
customerName: r.customerName,
customerEmail: r.customerEmail,
message: r.message,
typeName: r.type?.name ?? null,
optionName: r.option?.name ?? null,
extrasCount: r.extras.length,
filesCount: r.files.length,
};
initial[col].push(item);
}
return <CommissionsKanbanClient initialColumns={initial} />;
}

View File

@ -11,17 +11,21 @@ export default async function CommissionTypesEditPage({ params }: { params: { id
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
customInputs: { include: { customInput: true }, orderBy: { sortIndex: "asc" } },
tags: true,
},
})
const tags = await prisma.tag.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
});
const options = await prisma.commissionOption.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
});
const extras = await prisma.commissionExtra.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})
const customInputs = await prisma.commissionCustomInput.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})
// const customInputs = await prisma.commissionCustomInput.findMany({
// orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
// })
if (!commissionType) {
return <div>Type not found</div>
@ -32,7 +36,12 @@ export default async function CommissionTypesEditPage({ params }: { params: { id
<div className="flex gap-4 justify-between pb-8">
<h1 className="text-2xl font-bold mb-4">Edit Commission Type</h1>
</div>
<EditTypeForm type={commissionType} allOptions={options} allExtras={extras} allCustomInputs={customInputs} />
<EditTypeForm
type={commissionType}
allOptions={options}
allExtras={extras}
allTags={tags}
/>
</div>
);
}
}

View File

@ -0,0 +1,10 @@
import { ExtraListClient } from "@/components/commissions/extras/ExtraListClient";
import { prisma } from "@/lib/prisma";
export default async function CommissionTypesExtrasPage() {
const extras = await prisma.commissionExtra.findMany({
orderBy: [{ createdAt: "asc" }, { name: "asc" }],
});
return <ExtraListClient extras={extras} />;
}

View File

@ -2,6 +2,9 @@ import NewTypeForm from "@/components/commissions/types/NewTypeForm";
import { prisma } from "@/lib/prisma";
export default async function CommissionTypesNewPage() {
const tags = await prisma.tag.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
});
const options = await prisma.commissionOption.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
});
@ -17,8 +20,13 @@ export default async function CommissionTypesNewPage() {
<div className="flex gap-4 justify-between pb-8">
<h1 className="text-2xl font-bold mb-4">New Commission Type</h1>
</div>
<NewTypeForm options={options} extras={extras} customInputs={customInputs} />
<NewTypeForm
options={options}
extras={extras}
customInputs={customInputs}
tags={tags}
/>
</div>
);
}
}

View File

@ -0,0 +1,10 @@
import { OptionsListClient } from "@/components/commissions/options/OptionsListClient";
import { prisma } from "@/lib/prisma";
export default async function CommissionTypesOptionsPage() {
const options = await prisma.commissionOption.findMany({
orderBy: [{ createdAt: "asc" }, { name: "asc" }],
});
return <OptionsListClient options={options} />;
}

View File

@ -24,4 +24,4 @@ export default async function CommissionTypesPage() {
{types && types.length > 0 ? <ListTypes types={types} /> : <p className="text-muted-foreground italic">No types found.</p>}
</div>
);
}
}

View File

@ -1,6 +1,8 @@
import LogoutButton from "@/components/auth/LogoutButton";
import Footer from "@/components/global/Footer";
import Header from "@/components/global/Header";
import { Toaster } from "@/components/ui/sonner";
import MobileSidebar from "@/components/global/MobileSidebar";
import ModeToggle from "@/components/global/ModeToggle";
import Sidebar from "@/components/global/Sidebar";
export default function AdminLayout({
children,
@ -8,17 +10,48 @@ export default function AdminLayout({
children: React.ReactNode;
}>) {
return (
<div className="flex flex-col min-h-screen min-w-screen">
<header className="sticky top-0 z-50 h-14 w-full border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-4 py-2">
<Header />
</header>
<main className="container mx-auto px-4 py-8">
{children}
</main>
<footer className="mt-auto px-4 py-2 h-14 border-t bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
<Footer />
</footer>
<Toaster />
// <div className="flex flex-col min-h-screen min-w-screen">
// <header className="sticky top-0 z-50 h-14 w-full border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-4 py-2">
// <Header />
// </header>
// <main className="container mx-auto px-4 py-8">
// {children}
// </main>
// <footer className="mt-auto px-4 py-2 h-14 border-t bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
// <Footer />
// </footer>
// <Toaster />
// </div>
<div className="min-h-screen w-full">
<div className="flex min-h-screen w-full">
<aside className="hidden md:flex md:w-64 md:flex-col md:border-r md:bg-background">
<Sidebar />
</aside>
<div className="flex min-h-screen flex-1 flex-col">
<header className="sticky top-0 z-50 h-14 w-full border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
<div className="flex h-14 items-center gap-3 px-4">
<div className="md:hidden">
<MobileSidebar />
</div>
<div className="flex-1">
{/* Optional: put breadcrumbs or page title here later */}
</div>
<div className="flex items-center gap-3">
<LogoutButton />
<ModeToggle />
</div>
</div>
</header>
<main className="flex-1">
<div className="container mx-auto px-4 py-8">{children}</div>
</main>
<footer className="mt-auto h-14 border-t bg-background/95 px-4 py-2 backdrop-blur supports-backdrop-filter:bg-background/60">
<Footer />
</footer>
</div>
</div>
</div>
);
}

View File

@ -3,23 +3,43 @@ import { prisma } from "@/lib/prisma";
export default async function PortfolioTagsEditPage({ params }: { params: { id: string } }) {
const { id } = await params;
const tag = await prisma.artTag.findUnique({
const tag = await prisma.tag.findUnique({
where: {
id,
},
include: {
categories: true,
categoryLinks: {
include: {
category: true,
parentTag: { select: { id: true, name: true } },
},
},
aliases: true
}
})
const categories = await prisma.artCategory.findMany({ include: { tags: true }, orderBy: { sortIndex: "asc" } });
const tags = await prisma.artTag.findMany({ where: { isParent: true }, orderBy: { sortIndex: "asc" } });
const categories = await prisma.artCategory.findMany({
include: { tagLinks: true },
orderBy: { sortIndex: "asc" },
});
const tags = await prisma.tag.findMany({ orderBy: { sortIndex: "asc" } });
const animalLink =
tag?.categoryLinks.find((link) => link.category.name === "Animal Studies") ??
null;
const tagWithMeta = tag
? {
...tag,
parentId: animalLink?.parentTagId ?? null,
isParent: animalLink?.isParent ?? false,
showOnAnimalPage: animalLink?.showOnAnimalPage ?? false,
}
: null;
return (
<div>
<h1 className="text-2xl font-bold mb-4">Edit Tag</h1>
{tag && <EditTagForm tag={tag} categories={categories} allTags={tags} />}
{tagWithMeta && <EditTagForm tag={tagWithMeta} categories={categories} allTags={tags} />}
</div>
);
}
}

View File

@ -2,8 +2,11 @@ import NewTagForm from "@/components/tags/NewTagForm";
import { prisma } from "@/lib/prisma";
export default async function PortfolioTagsNewPage() {
const categories = await prisma.artCategory.findMany({ include: { tags: true }, orderBy: { sortIndex: "asc" } });
const tags = await prisma.artTag.findMany({ where: { isParent: true }, orderBy: { sortIndex: "asc" } });
const categories = await prisma.artCategory.findMany({
include: { tagLinks: true },
orderBy: { sortIndex: "asc" },
});
const tags = await prisma.tag.findMany({ orderBy: { sortIndex: "asc" } });
return (
<div>
@ -11,4 +14,4 @@ export default async function PortfolioTagsNewPage() {
<NewTagForm categories={categories} allTags={tags} />
</div>
);
}
}

View File

@ -5,38 +5,61 @@ import { PlusCircleIcon } from "lucide-react";
import Link from "next/link";
export default async function ArtTagsPage() {
const items = await prisma.artTag.findMany({
const items = await prisma.tag.findMany({
include: {
parent: { select: { id: true, name: true } },
aliases: { select: { alias: true } },
categories: { select: { id: true, name: true } },
categoryLinks: {
include: {
category: { select: { id: true, name: true } },
parentTag: { select: { id: true, name: true } },
},
},
_count: { select: { artworks: true } },
},
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
});
const rows = items.map((tag) => {
const categories = tag.categoryLinks.map((link) => link.category);
const animalLink = tag.categoryLinks.find(
(link) => link.category.name === "Animal Studies",
);
return {
id: tag.id,
name: tag.name,
slug: tag.slug,
parent: animalLink?.parentTag ?? null,
isParent: animalLink?.isParent ?? false,
showOnAnimalPage: animalLink?.showOnAnimalPage ?? false,
aliases: tag.aliases,
categories,
_count: tag._count,
};
});
return (
<div className="mx-auto w-full max-w-7xl px-4 py-6">
<header className="mb-6 flex flex-col gap-3 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">
Art Tags
Tags
</h1>
<p className="text-sm text-muted-foreground">
Manage tags, aliases, categories, and usage across artworks.
</p>
<p className="text-sm text-muted-foreground">Manage tags.</p>
</div>
<Button asChild className="h-11 gap-2">
<Link href="/tags/new">
<PlusCircleIcon className="h-4 w-4" />
Add new tag
</Link>
</Button>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Button asChild className="h-11 gap-2">
<Link href="/tags/new">
<PlusCircleIcon className="h-4 w-4" />
Add new tag
</Link>
</Button>
</div>
</header>
{items.length > 0 ? (
<TagTabs tags={items} />
{rows.length > 0 ? (
<TagTabs tags={rows} />
) : (
<p className="text-muted-foreground">
There are no tags yet. Consider adding some!
@ -44,4 +67,4 @@ export default async function ArtTagsPage() {
)}
</div>
);
}
}

View File

@ -7,6 +7,7 @@ import { z } from "zod/v4";
const payloadSchema = z.object({
typeId: z.string().min(1).optional().nullable(),
customCardId: z.string().min(1).optional().nullable(),
optionId: z.string().min(1).optional().nullable(),
extraIds: z.array(z.string().min(1)).default([]),
@ -14,6 +15,23 @@ const payloadSchema = z.object({
customerEmail: z.string().email().max(320),
customerSocials: z.string().max(2000).optional().nullable(),
message: z.string().min(1).max(20_000),
}).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",
});
}
});
function safeJsonParse(input: string) {
@ -83,6 +101,7 @@ export async function POST(request: Request) {
message: payload.data.message,
typeId: payload.data.typeId ?? null,
customCardId: payload.data.customCardId ?? null,
optionId: payload.data.optionId ?? null,
ipAddress,

View File

@ -0,0 +1,62 @@
"use client";
import { generateGalleryVariantsMissing } from "@/actions/artworks/generateGalleryVariant";
import { getGalleryVariantStats } from "@/actions/artworks/getGalleryVariantStats";
import { Button } from "@/components/ui/button";
import * as React from "react";
export function ArtworkGalleryVariantProcessor() {
const [stats, setStats] = React.useState<Awaited<
ReturnType<typeof getGalleryVariantStats>
> | null>(null);
const [loading, setLoading] = React.useState(false);
const [msg, setMsg] = React.useState<string | null>(null);
const refreshStats = React.useCallback(async () => {
const s = await getGalleryVariantStats();
setStats(s);
}, []);
React.useEffect(() => {
void refreshStats();
}, [refreshStats]);
const run = async () => {
setLoading(true);
setMsg(null);
try {
const res = await generateGalleryVariantsMissing({ limit: 50 });
setMsg(`Processed ${res.processed}: ${res.ok} ok, ${res.failed} failed`);
await refreshStats();
} catch (e) {
setMsg(e instanceof Error ? e.message : "Failed");
} finally {
setLoading(false);
}
};
const done = !!stats && stats.missing === 0;
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-4">
<Button onClick={run} disabled={loading || done}>
{done
? "All gallery variants present"
: loading
? "Generating…"
: "Generate missing gallery variants"}
</Button>
{stats && (
<span className="text-sm text-muted-foreground">
Ready {stats.withGallery}/{stats.total}
{stats.missing > 0 && ` · Missing ${stats.missing}`}
</span>
)}
</div>
{msg && <p className="text-sm text-muted-foreground">{msg}</p>}
</div>
);
}

View File

@ -1,10 +1,10 @@
"use client";
import {
ColumnDef,
SortingState,
type ColumnDef,
flexRender,
getCoreRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import {
@ -65,7 +65,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ArtworkTableRow } from "@/schemas/artworks/tableSchema";
import type { ArtworkTableRow } from "@/schemas/artworks/tableSchema";
import { MultiSelectFilter } from "./MultiSelectFilter";
type TriState = "any" | "true" | "false";
@ -103,9 +103,15 @@ function SortHeader(props: {
);
}
function YesNoBadge(props: { value: boolean; variant?: "default" | "secondary" }) {
function YesNoBadge(props: {
value: boolean;
variant?: "default" | "secondary";
}) {
return (
<Badge variant={props.value ? "default" : "secondary"} className="px-2 py-0.5">
<Badge
variant={props.value ? "default" : "secondary"}
className="px-2 py-0.5"
>
{props.value ? "Yes" : "No"}
</Badge>
);
@ -140,7 +146,10 @@ function TriSelectInline(props: {
onChange: (v: TriState) => void;
}) {
return (
<Select value={props.value} onValueChange={(v) => props.onChange(v as TriState)}>
<Select
value={props.value}
onValueChange={(v) => props.onChange(v as TriState)}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
@ -165,7 +174,7 @@ type Filters = {
export function ArtworksTable() {
const [sorting, setSorting] = React.useState<SortingState>([
{ id: "createdAt", desc: true },
{ id: "updatedAt", desc: true },
]);
const [pageIndex, setPageIndex] = React.useState(0);
const [pageSize, setPageSize] = React.useState(25);
@ -186,14 +195,20 @@ export function ArtworksTable() {
const [rows, setRows] = React.useState<ArtworkTableRow[]>([]);
const [total, setTotal] = React.useState(0);
const [albumOptions, setAlbumOptions] = React.useState<{ id: string; name: string }[]>([]);
const [categoryOptions, setCategoryOptions] = React.useState<{ id: string; name: string }[]>([]);
const [albumOptions, setAlbumOptions] = React.useState<
{ id: string; name: string }[]
>([]);
const [categoryOptions, setCategoryOptions] = React.useState<
{ id: string; name: string }[]
>([]);
const [isPending, startTransition] = React.useTransition();
// Delete dialog
const [deleteOpen, setDeleteOpen] = React.useState(false);
const [deleteTarget, setDeleteTarget] = React.useState<{ id: string; name: string } | null>(null);
const [deleteTarget, setDeleteTarget] = React.useState<{
id: string;
name: string;
} | null>(null);
const pageCount = Math.max(1, Math.ceil(total / pageSize));
@ -203,7 +218,6 @@ export function ArtworksTable() {
setAlbumOptions(res.albums);
setCategoryOptions(res.categories);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const columns = React.useMemo<ColumnDef<ArtworkTableRow>[]>(
@ -240,21 +254,33 @@ export function ArtworksTable() {
/>
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold">{row.original.name}</div>
<div className="truncate text-xs text-muted-foreground">{row.original.slug}</div>
<div className="truncate text-sm font-semibold">
{row.original.name}
</div>
<div className="truncate text-xs text-muted-foreground">
{row.original.slug}
</div>
<div className="mt-3 flex flex-wrap gap-2">
<YesNoBadge value={row.original.published} />
<YesNoBadge value={row.original.nsfw} variant="secondary" />
<YesNoBadge value={row.original.needsWork} variant="secondary" />
<YesNoBadge
value={row.original.nsfw}
variant="secondary"
/>
<YesNoBadge
value={row.original.needsWork}
variant="secondary"
/>
</div>
<div className="mt-3 space-y-2 text-xs">
<div className="text-muted-foreground">
Created: {new Date(row.original.createdAt).toLocaleString()}
Created:{" "}
{new Date(row.original.createdAt).toLocaleString()}
</div>
<div className="text-muted-foreground">
Updated: {new Date(row.original.updatedAt).toLocaleString()}
Updated:{" "}
{new Date(row.original.updatedAt).toLocaleString()}
</div>
</div>
</div>
@ -299,18 +325,28 @@ export function ArtworksTable() {
accessorKey: "albumsCount",
cell: ({ row }) => (
<div className="space-y-1">
<div className="text-sm font-medium tabular-nums">{row.original.albumsCount}</div>
{row.original.albums.length ? <Chips items={row.original.albums} /> : <span className="text-xs text-muted-foreground"></span>}
<div className="text-sm font-medium tabular-nums">
{row.original.albumsCount}
</div>
{row.original.albums.length ? (
<Chips items={row.original.albums} />
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</div>
),
},
{
id: "categories",
header: ({ column }) => <SortHeader title="Categories #" column={column} />,
header: ({ column }) => (
<SortHeader title="Categories #" column={column} />
),
accessorKey: "categoriesCount",
cell: ({ row }) => (
<div className="space-y-1">
<div className="text-sm font-medium tabular-nums">{row.original.categoriesCount}</div>
<div className="text-sm font-medium tabular-nums">
{row.original.categoriesCount}
</div>
{row.original.categories.length ? (
<Chips items={row.original.categories} />
) : (
@ -321,25 +357,26 @@ export function ArtworksTable() {
},
{
accessorKey: "published",
header: ({ column }) => <SortHeader title="Published" column={column} />,
header: ({ column }) => (
<SortHeader title="Published" column={column} />
),
cell: ({ row }) => <YesNoBadge value={row.original.published} />,
},
{
accessorKey: "nsfw",
header: ({ column }) => <SortHeader title="NSFW" column={column} />,
cell: ({ row }) => <YesNoBadge value={row.original.nsfw} variant="secondary" />,
},
{
accessorKey: "needsWork",
header: ({ column }) => <SortHeader title="Needs Work" column={column} />,
cell: ({ row }) => <YesNoBadge value={row.original.needsWork} variant="secondary" />,
header: ({ column }) => (
<SortHeader title="Needs Work" column={column} />
),
cell: ({ row }) => (
<YesNoBadge value={row.original.needsWork} variant="secondary" />
),
},
{
accessorKey: "createdAt",
header: ({ column }) => <SortHeader title="Created" column={column} />,
accessorKey: "updatedAt",
header: ({ column }) => <SortHeader title="Updated" column={column} />,
cell: ({ row }) => (
<span className="text-sm text-foreground/80">
{new Date(row.original.createdAt).toLocaleDateString()}
{new Date(row.original.updatedAt).toLocaleDateString()}
</span>
),
},
@ -362,7 +399,10 @@ export function ArtworksTable() {
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuItem asChild>
<Link href={`/artworks/${item.id}`} className="cursor-pointer">
<Link
href={`/artworks/${item.id}`}
className="cursor-pointer"
>
<Pencil className="mr-2 h-4 w-4" />
Edit
</Link>
@ -397,7 +437,9 @@ export function ArtworksTable() {
manualSorting: true,
pageCount,
onSortingChange: (updater) => {
setSorting((prev) => (typeof updater === "function" ? updater(prev) : updater));
setSorting((prev) =>
typeof updater === "function" ? updater(prev) : updater,
);
setPageIndex(0);
},
getCoreRowModel: getCoreRowModel(),
@ -412,10 +454,11 @@ export function ArtworksTable() {
name: debouncedName || undefined,
slug: debouncedSlug || undefined,
published: filters.published,
nsfw: filters.nsfw,
needsWork: filters.needsWork,
albumIds: filters.albumIds.length ? filters.albumIds : undefined,
categoryIds: filters.categoryIds.length ? filters.categoryIds : undefined,
categoryIds: filters.categoryIds.length
? filters.categoryIds
: undefined,
},
});
@ -429,7 +472,6 @@ export function ArtworksTable() {
debouncedName,
debouncedSlug,
filters.published,
filters.nsfw,
filters.needsWork,
filters.albumIds,
filters.categoryIds,
@ -453,7 +495,10 @@ export function ArtworksTable() {
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
@ -464,7 +509,10 @@ export function ArtworksTable() {
const colId = header.column.id;
return (
<TableHead key={header.id} className="border-b border-border/70 bg-muted/30 py-2">
<TableHead
key={header.id}
className="border-b border-border/70 bg-muted/30 py-2"
>
{colId === "name" ? (
<Input
className="h-9"
@ -493,14 +541,6 @@ export function ArtworksTable() {
setPageIndex(0);
}}
/>
) : colId === "nsfw" ? (
<TriSelectInline
value={filters.nsfw}
onChange={(v) => {
setFilters((f) => ({ ...f, nsfw: v }));
setPageIndex(0);
}}
/>
) : colId === "needsWork" ? (
<TriSelectInline
value={filters.needsWork}
@ -541,7 +581,10 @@ export function ArtworksTable() {
<TableBody>
{rows.length === 0 ? (
<TableRow>
<TableCell colSpan={columns.length} className="py-14 text-center">
<TableCell
colSpan={columns.length}
className="py-14 text-center"
>
<div className="text-sm font-medium">
{isPending ? "Loading…" : "No results."}
</div>
@ -561,8 +604,14 @@ export function ArtworksTable() {
].join(" ")}
>
{r.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-3 align-top border-b border-border/40">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
<TableCell
key={cell.id}
className="py-3 align-top border-b border-border/40"
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
@ -645,7 +694,9 @@ export function ArtworksTable() {
<AlertDialogHeader>
<AlertDialogTitle>Delete artwork?</AlertDialogTitle>
<AlertDialogDescription>
This will delete <span className="font-medium">{deleteTarget?.name}</span>. This action cannot be undone.
This will delete{" "}
<span className="font-medium">{deleteTarget?.name}</span>. This
action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
@ -670,10 +721,13 @@ export function ArtworksTable() {
name: debouncedName || undefined,
slug: debouncedSlug || undefined,
published: filters.published,
nsfw: filters.nsfw,
needsWork: filters.needsWork,
albumIds: filters.albumIds.length ? filters.albumIds : undefined,
categoryIds: filters.categoryIds.length ? filters.categoryIds : undefined,
albumIds: filters.albumIds.length
? filters.albumIds
: undefined,
categoryIds: filters.categoryIds.length
? filters.categoryIds
: undefined,
},
});
setRows(res.rows);

View File

@ -0,0 +1,168 @@
"use client";
import * as React from "react";
import {
confirmArtworkTimelapseUpload,
createArtworkTimelapseUpload,
deleteArtworkTimelapse,
setArtworkTimelapseEnabled,
} from "@/actions/artworks/timelapse";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { toast } from "sonner";
type Timelapse = {
enabled: boolean;
s3Key: string;
fileName: string | null;
mimeType: string | null;
sizeBytes: number | null;
};
function fmtBytes(bytes: number) {
const mb = bytes / (1024 * 1024);
if (mb >= 1024) return `${(mb / 1024).toFixed(2)} GB`;
return `${mb.toFixed(2)} MB`;
}
export default function ArtworkTimelapse({
artworkId,
timelapse,
}: {
artworkId: string;
timelapse: Timelapse | null;
}) {
const [isBusy, startTransition] = React.useTransition();
async function onPickFile(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
startTransition(async () => {
try {
// 1) create presigned url
const presign = await createArtworkTimelapseUpload({
artworkId,
fileName: file.name,
mimeType: file.type || "application/octet-stream",
sizeBytes: file.size,
});
// 2) upload to S3 directly
const putRes = await fetch(presign.uploadUrl, {
method: "PUT",
headers: {
"Content-Type": presign.mimeType,
},
body: file,
});
if (!putRes.ok) {
throw new Error(`Upload failed (${putRes.status})`);
}
// 3) persist in DB
await confirmArtworkTimelapseUpload({
artworkId,
s3Key: presign.s3Key,
fileName: presign.fileName,
mimeType: presign.mimeType,
sizeBytes: presign.sizeBytes,
});
toast.success("Timelapse uploaded");
e.target.value = "";
} catch (err) {
toast.error(err instanceof Error ? err.message : "Upload failed");
}
});
}
function onToggleEnabled(next: boolean) {
startTransition(async () => {
try {
await setArtworkTimelapseEnabled({ artworkId, enabled: next });
toast.success("Timelapse updated");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Update failed");
}
});
}
function onDelete() {
startTransition(async () => {
try {
await deleteArtworkTimelapse({ artworkId });
toast.success("Timelapse deleted");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Delete failed");
}
});
}
const src = timelapse?.s3Key ? `/api/image/${timelapse.s3Key}` : null;
return (
<div className="rounded-md border p-4 space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<div className="font-medium">Timelapse</div>
<div className="text-sm text-muted-foreground">
Upload a timelapse video for this artwork (stored in S3).
</div>
</div>
<div className="flex items-center gap-3">
<Label htmlFor="timelapse-enabled" className="text-sm">
Display
</Label>
<Switch
id="timelapse-enabled"
disabled={!timelapse || isBusy}
checked={timelapse?.enabled ?? false}
onCheckedChange={onToggleEnabled}
/>
</div>
</div>
<div className="flex items-center justify-between gap-4">
<div className="text-sm">
{timelapse ? (
<div className="space-y-1">
<div>
<span className="text-muted-foreground">File:</span>{" "}
{timelapse.fileName ?? timelapse.s3Key}
</div>
{typeof timelapse.sizeBytes === "number" ? (
<div className="text-muted-foreground">Size: {fmtBytes(timelapse.sizeBytes)}</div>
) : null}
</div>
) : (
<div className="text-muted-foreground">No timelapse uploaded yet.</div>
)}
</div>
<div className="flex items-center gap-2">
<input
type="file"
accept="video/*"
onChange={onPickFile}
disabled={isBusy}
className="block text-sm"
/>
{timelapse ? (
<Button variant="destructive" onClick={onDelete} disabled={isBusy}>
Delete
</Button>
) : null}
</div>
</div>
{src ? (
<video className="w-full rounded-md border" controls preload="metadata" src={src} />
) : null}
</div>
);
}

View File

@ -1,12 +1,19 @@
import { FileVariant } from "@/generated/prisma/client";
"use client";
import { generateGalleryVariant } from "@/actions/artworks/generateGalleryVariant";
import { Button } from "@/components/ui/button";
import type { FileVariant } from "@/generated/prisma/client";
import { formatFileSize } from "@/utils/formatFileSize";
import NextImage from "next/image";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
const ORDER: Record<string, number> = {
thumbnail: 0,
resized: 1,
modified: 2,
original: 3,
gallery: 1,
resized: 2,
modified: 3,
original: 4,
};
function byVariantOrder(a: FileVariant, b: FileVariant) {
@ -16,22 +23,58 @@ function byVariantOrder(a: FileVariant, b: FileVariant) {
return a.type.localeCompare(b.type);
}
export default function ArtworkVariants({ variants }: { variants: FileVariant[] }) {
export default function ArtworkVariants({
artworkId,
variants,
}: {
artworkId: string;
variants: FileVariant[];
}) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const hasGallery = variants.some((v) => v.type === "gallery");
const sorted = [...variants].sort(byVariantOrder);
return (
<>
<h2 className="font-semibold text-lg mb-2">Variants</h2>
<div className="mb-2 flex items-center justify-between gap-2">
<h2 className="font-semibold text-lg">Variants</h2>
{!hasGallery ? (
<Button
type="button"
size="sm"
variant="outline"
disabled={isPending}
onClick={() =>
startTransition(async () => {
await generateGalleryVariant(artworkId);
router.refresh();
})
}
>
{isPending ? "Generating..." : "Generate gallery"}
</Button>
) : null}
</div>
<div>
{sorted.map((variant) => (
<div key={variant.id}>
<div className="text-sm mb-1">{variant.type} | {variant.width}x{variant.height}px | {variant.sizeBytes ? formatFileSize(variant.sizeBytes) : "-"}</div>
<div className="text-sm mb-1">
{variant.type} | {variant.width}x{variant.height}px |{" "}
{variant.sizeBytes ? formatFileSize(variant.sizeBytes) : "-"}
</div>
{variant.s3Key && (
<NextImage src={`/api/image/${variant.s3Key}`} alt={variant.s3Key} width={variant.width} height={variant.height} className="rounded shadow max-w-md" />
<NextImage
src={`/api/image/${variant.s3Key}`}
alt={variant.s3Key}
width={variant.width}
height={variant.height}
className="rounded shadow max-w-md"
/>
)}
</div>
))}
</div>
</>
);
}
}

View File

@ -2,29 +2,25 @@
import { updateArtwork } from "@/actions/artworks/updateArtwork";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import MultipleSelector from "@/components/ui/multiselect";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { ArtTag } from "@/generated/prisma/client";
import { cn } from "@/lib/utils";
import type { Tag } from "@/generated/prisma/client";
import { artworkSchema } from "@/schemas/artworks/imageSchema";
import { ArtworkWithRelations, CategoryWithTags } from "@/types/Artwork";
import type { ArtworkWithRelations, CategoryWithTags } from "@/types/Artwork";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod/v4";
import type { z } from "zod/v4";
export default function EditArtworkForm({ artwork, categories, tags }:
{
artwork: ArtworkWithRelations,
categories: CategoryWithTags[]
tags: ArtTag[]
tags: Tag[]
}) {
const router = useRouter();
const form = useForm<z.infer<typeof artworkSchema>>({
@ -82,7 +78,7 @@ export default function EditArtworkForm({ artwork, categories, tags }:
<FormItem>
<FormLabel>Alt Text</FormLabel>
<FormControl>
<Input {...field} placeholder="Alt for this image" />
<Textarea {...field} placeholder="Alt for this image" />
</FormControl>
<FormMessage />
</FormItem>
@ -155,7 +151,7 @@ export default function EditArtworkForm({ artwork, categories, tags }:
</FormItem>
)}
/>
{/* Date */}
{/* Date
<FormField
control={form.control}
name="creationDate"
@ -195,7 +191,7 @@ export default function EditArtworkForm({ artwork, categories, tags }:
<FormMessage />
</FormItem>
)}
/>
/> */}
{/* Select */}
<FormField
control={form.control}
@ -268,7 +264,7 @@ export default function EditArtworkForm({ artwork, categories, tags }:
const preferredTagIds = new Set<string>();
for (const cat of categories) {
if (!selectedCategoryIds.includes(cat.id)) continue;
for (const t of cat.tags) preferredTagIds.add(t.id);
for (const link of cat.tagLinks) preferredTagIds.add(link.tagId);
}
// Existing tag options with groups
@ -287,6 +283,12 @@ export default function EditArtworkForm({ artwork, categories, tags }:
.filter((t) => selectedTagIds.includes(t.id))
.map((t) => ({ label: t.name, value: t.id }));
const fallbackSelectedOptions =
artwork.tags
?.filter((t) => selectedTagIds.includes(t.id))
.filter((t) => !selectedExistingOptions.some((o) => o.value === t.id))
.map((t) => ({ label: t.name, value: t.id })) ?? [];
// Selected "new" tags (so they remain visible)
const selectedNewOptions = newTagNames.map((name) => ({
label: `Create: ${name}`,
@ -306,7 +308,11 @@ export default function EditArtworkForm({ artwork, categories, tags }:
placeholder="Select or type to create tags"
hidePlaceholderWhenSelected
selectFirstItem
value={[...selectedExistingOptions, ...selectedNewOptions]}
value={[
...selectedExistingOptions,
...fallbackSelectedOptions,
...selectedNewOptions,
]}
creatable
createOption={(raw) => ({
value: `__new__:${raw}`,
@ -337,21 +343,6 @@ export default function EditArtworkForm({ artwork, categories, tags }:
/>
{/* Boolean */}
<FormField
control={form.control}
name="needsWork"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Needs some work</FormLabel>
<FormDescription></FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="nsfw"
@ -367,21 +358,6 @@ export default function EditArtworkForm({ artwork, categories, tags }:
</FormItem>
)}
/>
<FormField
control={form.control}
name="published"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Publish</FormLabel>
<FormDescription>Will this image be published.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="setAsHeader"
@ -397,51 +373,37 @@ export default function EditArtworkForm({ artwork, categories, tags }:
</FormItem>
)}
/>
{/* Read only */}
{/* <FormField
<FormField
control={form.control}
name="fileKey"
name="needsWork"
render={({ field }) => (
<FormItem>
<FormLabel>Image Key</FormLabel>
<FormControl><Input {...field} disabled /></FormControl>
<FormMessage />
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Needs some work</FormLabel>
<FormDescription></FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="originalFile"
name="published"
render={({ field }) => (
<FormItem>
<FormLabel>Original file</FormLabel>
<FormControl><Input {...field} disabled /></FormControl>
<FormMessage />
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Publish</FormLabel>
<FormDescription>Will this image be published.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="fileType"
render={({ field }) => (
<FormItem>
<FormLabel>Filetype</FormLabel>
<FormControl><Input {...field} disabled /></FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fileSize"
render={({ field }) => (
<FormItem>
<FormLabel>FileSize</FormLabel>
<FormControl><Input type="number" {...field} disabled /></FormControl>
<FormMessage />
</FormItem>
)}
/> */}
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
@ -450,4 +412,4 @@ export default function EditArtworkForm({ artwork, categories, tags }:
</Form>
</div >
);
}
}

View File

@ -17,7 +17,7 @@ type CatRow = {
id: string;
name: string;
slug: string;
_count: { artworks: number, tags: number };
_count: { artworks: number, tagLinks: number };
};
export default function CategoryTable({ categories }: { categories: CatRow[] }) {
@ -48,7 +48,7 @@ export default function CategoryTable({ categories }: { categories: CatRow[] })
</TableCell>
<TableCell className="text-right tabular-nums">
{c._count.tags}
{c._count.tagLinks}
</TableCell>
<TableCell className="text-right tabular-nums">

View File

@ -0,0 +1,116 @@
"use client";
import {
deleteCommissionCustomCardImage,
uploadCommissionCustomCardImage,
} from "@/actions/commissions/customCards/images";
import { Button } from "@/components/ui/button";
import {
FormControl,
FormDescription,
FormItem,
FormLabel,
} from "@/components/ui/form";
import type { CommissionCustomCardValues } from "@/schemas/commissionCustomCard";
import Image from "next/image";
import { useMemo, useTransition } from "react";
import type { UseFormReturn } from "react-hook-form";
import { useWatch } from "react-hook-form";
type Props = {
form: UseFormReturn<CommissionCustomCardValues>;
initialImages: { key: string; url: string }[];
};
export function CustomCardImagePicker({ form }: Props) {
const [isPending, startTransition] = useTransition();
const referenceImageUrl = useWatch({
control: form.control,
name: "referenceImageUrl",
});
const previewUrl = useMemo(() => {
if (!referenceImageUrl) return "";
return referenceImageUrl;
}, [referenceImageUrl]);
const handleUpload = (file: File) => {
const fd = new FormData();
fd.append("file", file);
startTransition(async () => {
const item = await uploadCommissionCustomCardImage(fd);
form.setValue("referenceImageUrl", item.url, { shouldDirty: true });
});
};
const handleDelete = () => {
const url = referenceImageUrl ?? "";
const key = url.replace(/^\/api\/image\//, "");
const decodedKey = decodeURIComponent(key);
if (!decodedKey) return;
if (!window.confirm("Delete this image from S3?")) return;
startTransition(async () => {
await deleteCommissionCustomCardImage(decodedKey);
form.setValue("referenceImageUrl", null, { shouldDirty: true });
});
};
return (
<FormItem>
<FormLabel>Reference image</FormLabel>
<FormControl>
<div className="flex flex-col gap-2">
<input type="hidden" {...form.register("referenceImageUrl")} />
{previewUrl ? (
<div className="flex flex-col gap-2">
<div className="relative w-full max-w-md overflow-hidden rounded-lg border border-border/60 bg-muted/40">
<Image
src={previewUrl}
alt="Reference preview"
width={900}
height={600}
className="h-auto w-full object-cover"
/>
</div>
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary underline"
>
Open full size
</a>
</div>
) : (
<p className="text-sm text-muted-foreground">No image selected.</p>
)}
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleUpload(file);
e.currentTarget.value = "";
}}
/>
<Button
type="button"
variant="secondary"
onClick={handleDelete}
disabled={!referenceImageUrl || isPending}
>
Delete image
</Button>
</div>
</div>
</FormControl>
<FormDescription>
Upload and preview a reference image stored in the custom card bucket folder.
</FormDescription>
</FormItem>
);
}

View File

@ -0,0 +1,223 @@
"use client";
import { updateCommissionCustomCard } from "@/actions/commissions/customCards/updateCard";
import type { CommissionCustomCardImageItem } from "@/actions/commissions/customCards/images";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import type { CommissionExtra, CommissionOption, Tag } from "@/generated/prisma/client";
import {
commissionCustomCardSchema,
type CommissionCustomCardValues,
} from "@/schemas/commissionCustomCard";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { CommissionExtraField } from "../types/form/CommissionExtraField";
import { CommissionOptionField } from "../types/form/CommissionOptionField";
import { CustomCardImagePicker } from "./CustomCardImagePicker";
import MultipleSelector from "@/components/ui/multiselect";
type CustomCardOption = {
optionId: string;
price: number | null;
pricePercent: number | null;
priceRange: string | null;
};
type CustomCardExtra = {
extraId: string;
price: number | null;
pricePercent: number | null;
priceRange: string | null;
};
type CustomCardWithItems = {
id: string;
name: string;
description: string | null;
referenceImageUrl: string | null;
isVisible: boolean;
isSpecialOffer: boolean;
tags: Tag[];
options: CustomCardOption[];
extras: CustomCardExtra[];
};
type Props = {
card: CustomCardWithItems;
allOptions: CommissionOption[];
allExtras: CommissionExtra[];
images: CommissionCustomCardImageItem[];
allTags: Tag[];
};
export default function EditCustomCardForm({
card,
allOptions,
allExtras,
images,
allTags,
}: Props) {
const router = useRouter();
const form = useForm<CommissionCustomCardValues>({
resolver: zodResolver(commissionCustomCardSchema),
defaultValues: {
name: card.name,
description: card.description ?? "",
isVisible: card.isVisible,
isSpecialOffer: card.isSpecialOffer,
referenceImageUrl: card.referenceImageUrl ?? null,
tagIds: card.tags.map((t) => t.id),
options: card.options.map((o) => ({
optionId: o.optionId,
price: o.price ?? undefined,
pricePercent: o.pricePercent ?? undefined,
priceRange: o.priceRange ?? undefined,
})),
extras: card.extras.map((e) => ({
extraId: e.extraId,
price: e.price ?? undefined,
pricePercent: e.pricePercent ?? undefined,
priceRange: e.priceRange ?? undefined,
})),
},
});
async function onSubmit(values: CommissionCustomCardValues) {
try {
await updateCommissionCustomCard(card.id, values);
toast.success("Custom commission card updated.");
router.push("/commissions/custom-cards");
} catch (err) {
console.error(err);
toast("Failed to update custom commission card.");
}
}
return (
<div className="flex flex-col gap-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>The name of the custom commission card.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>Optional description.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isVisible"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Visible on app</FormLabel>
<FormDescription>Controls whether the card is shown.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="isSpecialOffer"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Special offer</FormLabel>
<FormDescription>Adds a special offer badge on the app.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="referenceImageUrl"
render={() => <CustomCardImagePicker form={form} initialImages={images} />}
/>
<FormField
control={form.control}
name="tagIds"
render={({ field }) => {
const selectedIds = field.value ?? [];
const selectedOptions = allTags
.filter((t) => selectedIds.includes(t.id))
.map((t) => ({ label: t.name, value: t.id }));
return (
<FormItem>
<FormLabel>Tags</FormLabel>
<FormControl>
<MultipleSelector
options={allTags.map((t) => ({ label: t.name, value: t.id }))}
placeholder="Select tags for this custom card"
hidePlaceholderWhenSelected
selectFirstItem
value={selectedOptions}
onChange={(options) => field.onChange(options.map((o) => o.value))}
/>
</FormControl>
<FormDescription>
Used to link this custom card to tagged artworks.
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<CommissionOptionField options={allOptions} />
<CommissionExtraField extras={allExtras} />
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>
Cancel
</Button>
</div>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,219 @@
"use client";
import { deleteCommissionCustomCard } from "@/actions/commissions/customCards/deleteCard";
import { updateCommissionCustomCardSortOrder } from "@/actions/commissions/customCards/updateSortOrder";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
closestCenter,
DndContext,
DragEndEvent,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { arrayMove, rectSortingStrategy, SortableContext } from "@dnd-kit/sortable";
import { PencilIcon, TrashIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useState, useTransition } from "react";
import SortableItemCard from "../types/SortableItemCard";
type CustomCardOption = {
id: string;
optionId: string;
price: number | null;
pricePercent: number | null;
priceRange: string | null;
option: { name: string } | null;
};
type CustomCardExtra = {
id: string;
extraId: 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;
isVisible: boolean;
isSpecialOffer: boolean;
options: CustomCardOption[];
extras: CustomCardExtra[];
};
export default function ListCustomCards({
cards,
}: {
cards: CommissionCustomCardWithItems[];
}) {
const [items, setItems] = useState(cards);
const [isMounted, setIsMounted] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
useEffect(() => {
setIsMounted(true);
}, []);
const sensors = useSensors(useSensor(PointerSensor));
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const oldIndex = items.findIndex((i) => i.id === active.id);
const newIndex = items.findIndex((i) => i.id === over.id);
if (oldIndex !== -1 && newIndex !== -1) {
const newItems = arrayMove(items, oldIndex, newIndex);
setItems(newItems);
await updateCommissionCustomCardSortOrder(
newItems.map((item, i) => ({ id: item.id, sortIndex: i }))
);
}
};
const confirmDelete = () => {
if (!deleteTargetId) return;
startTransition(async () => {
await deleteCommissionCustomCard(deleteTargetId);
setItems((prev) => prev.filter((i) => i.id !== deleteTargetId));
setDialogOpen(false);
setDeleteTargetId(null);
});
};
if (!isMounted) return null;
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={items.map((i) => i.id)} strategy={rectSortingStrategy}>
{items.map((card) => (
<SortableItemCard key={card.id} id={card.id}>
<Card>
<CardHeader className="relative">
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-xl truncate">{card.name}</CardTitle>
{!card.isVisible ? (
<Badge variant="secondary">Hidden</Badge>
) : (
<Badge variant="outline">Visible</Badge>
)}
{card.isSpecialOffer ? (
<Badge className="bg-amber-500 text-amber-950 hover:bg-amber-500">
Special
</Badge>
) : null}
</div>
<CardDescription>{card.description}</CardDescription>
{card.referenceImageUrl ? (
<p className="text-xs text-muted-foreground">Has image</p>
) : null}
</CardHeader>
<CardContent className="flex flex-col justify-start gap-4">
<div>
<h4 className="font-semibold">Options</h4>
<ul className="pl-4 list-disc">
{card.options.map((opt) => (
<li key={opt.id}>
{opt.option?.name}:{" "}
{opt.price !== null
? `${opt.price}`
: opt.pricePercent
? `+${opt.pricePercent}%`
: opt.priceRange
? `${opt.priceRange}`
: "Included"}
</li>
))}
</ul>
</div>
<div>
<h4 className="font-semibold">Extras</h4>
<ul className="pl-4 list-disc">
{card.extras.map((ext) => (
<li key={ext.id}>
{ext.extra?.name}:{" "}
{ext.price !== null
? `${ext.price}`
: ext.pricePercent
? `+${ext.pricePercent}%`
: ext.priceRange
? `${ext.priceRange}`
: "Included"}
</li>
))}
</ul>
</div>
</CardContent>
<CardFooter className="flex flex-col gap-2">
<Link href={`/commissions/custom-cards/${card.id}`} className="w-full">
<Button variant="default" className="w-full flex items-center gap-2">
<PencilIcon className="h-4 w-4" />
Edit
</Button>
</Link>
<Button
variant="destructive"
className="w-full flex items-center gap-2"
onClick={() => {
setDeleteTargetId(card.id);
setDialogOpen(true);
}}
>
<TrashIcon className="h-4 w-4" />
Delete
</Button>
</CardFooter>
</Card>
</SortableItemCard>
))}
</SortableContext>
</DndContext>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete this custom card?</DialogTitle>
</DialogHeader>
<p>This action cannot be undone. Are you sure you want to continue?</p>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogOpen(false)}>
Cancel
</Button>
<Button variant="destructive" disabled={isPending} onClick={confirmDelete}>
Confirm Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@ -0,0 +1,181 @@
"use client";
import type { CommissionCustomCardImageItem } from "@/actions/commissions/customCards/images";
import { createCommissionCustomCard } from "@/actions/commissions/customCards/newCard";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import type { CommissionExtra, CommissionOption, Tag } from "@/generated/prisma/client";
import {
commissionCustomCardSchema,
type CommissionCustomCardValues,
} from "@/schemas/commissionCustomCard";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { CommissionExtraField } from "../types/form/CommissionExtraField";
import { CommissionOptionField } from "../types/form/CommissionOptionField";
import { CustomCardImagePicker } from "./CustomCardImagePicker";
import MultipleSelector from "@/components/ui/multiselect";
type Props = {
options: CommissionOption[];
extras: CommissionExtra[];
images: CommissionCustomCardImageItem[];
tags: Tag[];
};
export default function NewCustomCardForm({ options, extras, images, tags }: Props) {
const router = useRouter();
const form = useForm<CommissionCustomCardValues>({
resolver: zodResolver(commissionCustomCardSchema),
defaultValues: {
name: "",
description: "",
isVisible: true,
isSpecialOffer: false,
referenceImageUrl: null,
tagIds: [],
options: [],
extras: [],
},
});
async function onSubmit(values: CommissionCustomCardValues) {
try {
const created = await createCommissionCustomCard(values);
console.log("Commission custom card created:", created);
toast("Custom commission card created.");
router.push("/commissions/custom-cards");
} catch (err) {
console.error(err);
toast("Failed to create custom commission card.");
}
}
return (
<div className="flex flex-col gap-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>The name of the custom commission card.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>Optional description.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isVisible"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Visible on app</FormLabel>
<FormDescription>Controls whether the card is shown.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="isSpecialOffer"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Special offer</FormLabel>
<FormDescription>Adds a special offer badge on the app.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="referenceImageUrl"
render={() => <CustomCardImagePicker form={form} initialImages={images} />}
/>
<FormField
control={form.control}
name="tagIds"
render={({ field }) => {
const selectedIds = field.value ?? [];
const selectedOptions = tags
.filter((t) => selectedIds.includes(t.id))
.map((t) => ({ label: t.name, value: t.id }));
return (
<FormItem>
<FormLabel>Tags</FormLabel>
<FormControl>
<MultipleSelector
options={tags.map((t) => ({ label: t.name, value: t.id }))}
placeholder="Select tags for this custom card"
hidePlaceholderWhenSelected
selectFirstItem
value={selectedOptions}
onChange={(options) => field.onChange(options.map((o) => o.value))}
/>
</FormControl>
<FormDescription>
Used to link this custom card to tagged artworks.
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<CommissionOptionField options={options} />
<CommissionExtraField extras={extras} />
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>
Cancel
</Button>
</div>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,122 @@
"use client";
import { createCommissionExtra, updateCommissionExtra } from "@/actions/commissions/types/extras";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { type CommissionExtraValues, commissionExtraSchema } from "@/schemas/commissionType";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
type Initial = {
id: string;
name: string;
description: string | null;
};
type Props = {
trigger: React.ReactNode;
mode: "create" | "edit";
initial?: Initial;
};
export function ExtraDialog({ trigger, mode, initial }: Props) {
const [open, setOpen] = useState(false);
// ✅ key remounts the form per item / create mode, no useEffect needed
const formKey = mode === "create" ? "new" : initial?.id ?? "missing";
const form = useForm<CommissionExtraValues>({
resolver: zodResolver(commissionExtraSchema),
defaultValues: {
name: initial?.name ?? "",
description: initial?.description ?? ""
},
});
async function onSubmit(values: CommissionExtraValues) {
try {
if (mode === "create") {
await createCommissionExtra(values);
toast.success("Extra created.");
} else {
if (!initial?.id) throw new Error("Missing extra id");
await updateCommissionExtra(initial.id, values);
toast.success("Extra updated.");
}
setOpen(false);
} catch (e) {
console.error(e);
toast.error("Failed to save extra.");
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{mode === "create" ? "New extra" : "Edit extra"}</DialogTitle>
</DialogHeader>
<div key={formKey}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormDescription>Shown to customers.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormDescription>Optional helper text.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="gap-2 sm:gap-0">
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="submit">Save</Button>
</DialogFooter>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,101 @@
"use client";
import { deleteCommissionExtra } from "@/actions/commissions/types/extras";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Pencil, Plus, Trash2 } from "lucide-react";
import * as React from "react";
import { toast } from "sonner";
import { ExtraDialog } from "./ExtraDialog";
type Item = {
id: string;
name: string;
description: string | null;
};
export function ExtraListClient({ extras }: { extras: Item[] }) {
const [busyId, setBusyId] = React.useState<string | null>(null);
async function onDelete(id: string) {
try {
setBusyId(id);
await deleteCommissionExtra(id);
toast.success("Extra deleted.");
} catch (e) {
console.error(e);
toast.error("Failed to delete extra.");
} finally {
setBusyId(null);
}
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between gap-4">
<h1 className="text-2xl font-bold">Extras</h1>
<ExtraDialog
mode="create"
trigger={
<Button className="gap-2">
<Plus className="h-4 w-4" />
New extra
</Button>
}
/>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">All extras</CardTitle>
</CardHeader>
<CardContent className="p-0">
{extras.length === 0 ? (
<div className="p-6 text-sm text-muted-foreground italic">No extras yet.</div>
) : (
<ul className="divide-y">
{extras.map((o) => (
<li key={o.id} className="flex items-center justify-between gap-4 px-6 py-4">
<div className="min-w-0">
<div className="flex items-center gap-2">
<div className="font-medium truncate">{o.name}</div>
</div>
{o.description ? (
<div className="text-sm text-muted-foreground truncate">{o.description}</div>
) : null}
</div>
<div className="flex items-center gap-2">
<ExtraDialog
mode="edit"
initial={o}
trigger={
<Button variant="secondary" size="sm" className="gap-2">
<Pencil className="h-4 w-4" />
Edit
</Button>
}
/>
<Button
variant="destructive"
size="sm"
className="gap-2"
disabled={busyId === o.id}
onClick={() => onDelete(o.id)}
>
<Trash2 className="h-4 w-4" />
Delete
</Button>
</div>
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
);
}

View File

@ -2,16 +2,20 @@
import type { Value } from 'platejs';
import { deleteCommissionExample, uploadCommissionExample } from "@/actions/commissions/examples";
import { saveGuidelines } from '@/actions/commissions/guidelines/saveGuidelines';
import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit';
import { BasicMarksKit } from '@/components/editor/plugins/basic-marks-kit';
import { CodeBlockKit } from '@/components/editor/plugins/code-block-kit';
import { ListKit } from '@/components/editor/plugins/list-kit';
import { MarkdownKit } from '@/components/editor/plugins/markdown-kit';
import { Button } from "@/components/ui/button";
import { Editor, EditorContainer } from '@/components/ui/editor';
import { FixedToolbar } from '@/components/ui/fixed-toolbar';
import { Label } from '@/components/ui/label';
import { BulletedListToolbarButton, NumberedListToolbarButton } from '@/components/ui/list-toolbar-button';
import { MarkToolbarButton } from '@/components/ui/mark-toolbar-button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ToolbarButton } from '@/components/ui/toolbar';
import {
Bold,
@ -27,14 +31,25 @@ import {
Underline
} from "lucide-react";
import { Plate, usePlateEditor } from 'platejs/react';
import { useEffect } from 'react';
import { useEffect, useMemo, useState, useTransition } from 'react';
const initialValue: Value = [
];
export default function GuidelinesEditor({ markdown }: { markdown: string | null }) {
export default function GuidelinesEditor({
markdown,
exampleImageUrl,
examples,
}: {
markdown: string | null;
exampleImageUrl: string | null;
examples: { key: string; url: string; size: number | null; lastModified: string | null }[];
}) {
// const [isSaving, setIsSaving] = useState(false);
const [exampleItems, setExampleItems] = useState(examples);
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const editor = usePlateEditor({
plugins: [
...BasicBlocksKit,
@ -54,17 +69,104 @@ export default function GuidelinesEditor({ markdown }: { markdown: string | null
}
}, [editor, markdown]);
useEffect(() => {
const match = exampleItems.find((item) => item.url === exampleImageUrl);
setSelectedKey(match?.key ?? null);
}, [exampleImageUrl, exampleItems]);
const selectedUrl = useMemo(() => {
if (!selectedKey) return "";
return exampleItems.find((item) => item.key === selectedKey)?.url ?? "";
}, [exampleItems, selectedKey]);
const handleSave = async () => {
// console.log(editor);
if (!editor.api.markdown.serialize) return;
// setIsSaving(true);
const markdown = editor.api.markdown.serialize();
await saveGuidelines(markdown);
await saveGuidelines(markdown, selectedUrl || null);
// setIsSaving(false);
};
const handleUpload = (file: File) => {
const fd = new FormData();
fd.append("file", file);
startTransition(async () => {
const item = await uploadCommissionExample(fd);
setExampleItems((prev) => [item, ...prev]);
setSelectedKey(item.key);
});
};
const handleDelete = () => {
if (!selectedKey) return;
if (!window.confirm("Delete this example image from S3?")) return;
startTransition(async () => {
await deleteCommissionExample(selectedKey);
setExampleItems((prev) => prev.filter((item) => item.key !== selectedKey));
setSelectedKey(null);
});
};
return (
<Plate editor={editor}> {/* Provides editor context */}
<div className="px-4 pt-4 flex flex-col gap-3">
<div className="flex flex-col gap-2">
<Label className="text-sm font-medium">Example image</Label>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Select
value={selectedKey ?? undefined}
onValueChange={(value) =>
setSelectedKey(value === "__none__" ? null : value)
}
>
<SelectTrigger className="w-full sm:max-w-md">
<SelectValue placeholder="Select an example image" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">None</SelectItem>
{exampleItems.map((item) => (
<SelectItem key={item.key} value={item.key}>
{item.key.replace("commissions/examples/", "")}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedUrl ? (
<a
href={selectedUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary underline"
>
Open
</a>
) : null}
</div>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleUpload(file);
e.currentTarget.value = "";
}}
/>
<Button
type="button"
variant="secondary"
onClick={handleDelete}
disabled={!selectedKey || isPending}
>
Delete selected
</Button>
</div>
</div>
<FixedToolbar className="justify-start rounded-t-lg">
{/* Blocks */}
<ToolbarButton onClick={() => editor.tf.h1.toggle()} tooltip="Heading 1">
@ -110,4 +212,4 @@ export default function GuidelinesEditor({ markdown }: { markdown: string | null
</EditorContainer>
</Plate>
);
}
}

View File

@ -0,0 +1,207 @@
"use client";
import { updateCommissionRequestStatus } from "@/actions/commissions/requests/updateCommissionRequestStatus";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Kanban, KanbanBoard, KanbanColumn, KanbanItem, KanbanOverlay } from "@/components/ui/kanban";
import {
BOARD_COLUMNS,
type BoardColumnId,
canonicalStatusForColumn,
} from "@/lib/commissions/kanban";
import Link from "next/link";
import * as React from "react";
type BoardItem = {
id: string;
createdAt: string;
status: string;
customerName: string;
customerEmail: string;
message: string;
typeName: string | null;
optionName: string | null;
extrasCount: number;
filesCount: number;
};
type ColumnsState = Record<BoardColumnId, BoardItem[]>;
import type { UniqueIdentifier } from "@dnd-kit/core";
type KanbanValue = Record<UniqueIdentifier, BoardItem[]>;
function isColumnsState(v: KanbanValue): v is ColumnsState {
return (
Array.isArray(v.intake) &&
Array.isArray(v.inProgress) &&
Array.isArray(v.completed)
);
}
function asColumnsState(v: KanbanValue): ColumnsState {
if (!isColumnsState(v)) {
// Defensive: if something ever changes upstream, keep UI stable
return { intake: [], inProgress: [], completed: [] };
}
return v;
}
function findItemColumn(columns: ColumnsState, itemId: string): BoardColumnId | null {
for (const col of Object.keys(columns) as BoardColumnId[]) {
if (columns[col].some((x) => x.id === itemId)) return col;
}
return null;
}
function diffMovedItem(prev: ColumnsState, next: ColumnsState) {
const prevLoc = new Map<string, BoardColumnId>();
const nextLoc = new Map<string, BoardColumnId>();
for (const c of Object.keys(prev) as BoardColumnId[]) {
for (const i of prev[c]) {
prevLoc.set(i.id, c);
}
}
for (const c of Object.keys(next) as BoardColumnId[]) {
for (const i of next[c]) {
nextLoc.set(i.id, c);
}
}
for (const [id, from] of prevLoc.entries()) {
const to = nextLoc.get(id);
if (to && to !== from) return { id, from, to };
}
return null;
}
export default function CommissionsKanbanClient({
initialColumns,
}: {
initialColumns: ColumnsState;
}) {
const [columns, setColumns] = React.useState<ColumnsState>(initialColumns);
const prevRef = React.useRef(columns);
React.useEffect(() => {
prevRef.current = columns;
}, [columns]);
async function persistMove(moved: { id: string; to: BoardColumnId }, snapshotBefore: ColumnsState) {
const status = canonicalStatusForColumn(moved.to);
try {
await updateCommissionRequestStatus({ id: moved.id, status });
// optional: you could also update the items status in local state here
// but revalidatePath + eventual refresh will keep it consistent anyway.
setColumns((cur) => {
const col = findItemColumn(cur, moved.id);
if (!col) return cur;
return {
...cur,
[col]: cur[col].map((x) => (x.id === moved.id ? { ...x, status } : x)),
};
});
} catch {
// Revert optimistic state if update fails
setColumns(snapshotBefore);
}
}
function onValueChange(next: KanbanValue) {
const nextColumns = asColumnsState(next);
const prev = prevRef.current;
setColumns(nextColumns);
const moved = diffMovedItem(prev, nextColumns);
if (!moved) return;
void persistMove({ id: moved.id, to: moved.to }, prev);
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between gap-3">
<div>
<h1 className="text-lg font-semibold">Commissions Board</h1>
<p className="text-sm text-muted-foreground">
Drag requests between columns to update their status.
</p>
</div>
<Button asChild variant="outline">
<Link href="/commissions/requests">Open list</Link>
</Button>
</div>
<Kanban
value={columns as unknown as KanbanValue}
onValueChange={onValueChange}
getItemValue={(item) => item.id}
>
<KanbanBoard className="grid auto-rows-fr gap-3 lg:grid-cols-3">
{(Object.keys(columns) as BoardColumnId[]).map((colId) => {
const col = BOARD_COLUMNS[colId];
const items = columns[colId];
return (
<KanbanColumn key={colId} value={colId} className="rounded-lg border bg-card/50 p-3">
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold">{col.title}</span>
<Badge variant="secondary" className="rounded-sm">
{items.length}
</Badge>
</div>
</div>
<div className="flex flex-col gap-2">
{items.map((item) => (
<KanbanItem key={item.id} value={item.id} asHandle asChild>
<div className="rounded-md border bg-background p-3 shadow-xs">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="truncate text-sm font-medium">
{item.customerName}
<span className="text-muted-foreground"> #{item.id.slice(0, 6)}</span>
</div>
<div className="truncate text-xs text-muted-foreground">
{item.typeName ?? "No type"}{item.optionName ? ` · ${item.optionName}` : ""}
</div>
</div>
<Badge variant="outline" className="rounded-sm text-[11px]">
{item.status}
</Badge>
</div>
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">
{item.message}
</p>
<div className="mt-3 flex items-center justify-between text-xs text-muted-foreground">
<span>
Files: {item.filesCount} · Extras: {item.extrasCount}
</span>
<Button asChild size="sm" variant="ghost" className="h-7 px-2">
<Link href={`/commissions/requests/${item.id}`}>Details</Link>
</Button>
</div>
</div>
</KanbanItem>
))}
</div>
</KanbanColumn>
);
})}
</KanbanBoard>
<KanbanOverlay>
<div className="size-full rounded-md bg-primary/10" />
</KanbanOverlay>
</Kanban>
</div>
);
}

View File

@ -0,0 +1,121 @@
"use client";
import { createCommissionOption, updateCommissionOption } from "@/actions/commissions/types/options";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { type CommissionOptionValues, commissionOptionSchema } from "@/schemas/commissionType";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
type Initial = {
id: string;
name: string;
description: string | null;
};
type Props = {
trigger: React.ReactNode;
mode: "create" | "edit";
initial?: Initial;
};
export function OptionDialog({ trigger, mode, initial }: Props) {
const [open, setOpen] = useState(false);
const formKey = mode === "create" ? "new" : initial?.id ?? "missing";
const form = useForm<CommissionOptionValues>({
resolver: zodResolver(commissionOptionSchema),
defaultValues: {
name: initial?.name ?? "",
description: initial?.description ?? ""
},
});
async function onSubmit(values: CommissionOptionValues) {
try {
if (mode === "create") {
await createCommissionOption(values);
toast.success("Option created.");
} else {
if (!initial?.id) throw new Error("Missing option id");
await updateCommissionOption(initial.id, values);
toast.success("Option updated.");
}
setOpen(false);
} catch (e) {
console.error(e);
toast.error("Failed to save option.");
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{mode === "create" ? "New option" : "Edit option"}</DialogTitle>
</DialogHeader>
<div key={formKey}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormDescription>Shown to customers.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormDescription>Optional helper text.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="gap-2 sm:gap-0">
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="submit">Save</Button>
</DialogFooter>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,101 @@
"use client";
import { deleteCommissionOption } from "@/actions/commissions/types/options";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Pencil, Plus, Trash2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { OptionDialog } from "./OptionDialog";
type Item = {
id: string;
name: string;
description: string | null;
};
export function OptionsListClient({ options }: { options: Item[] }) {
const [busyId, setBusyId] = useState<string | null>(null);
async function onDelete(id: string) {
try {
setBusyId(id);
await deleteCommissionOption(id);
toast.success("Option deleted.");
} catch (e) {
console.error(e);
toast.error("Failed to delete option.");
} finally {
setBusyId(null);
}
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between gap-4">
<h1 className="text-2xl font-bold">Options</h1>
<OptionDialog
mode="create"
trigger={
<Button className="gap-2">
<Plus className="h-4 w-4" />
New option
</Button>
}
/>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">All options</CardTitle>
</CardHeader>
<CardContent className="p-0">
{options.length === 0 ? (
<div className="p-6 text-sm text-muted-foreground italic">No options yet.</div>
) : (
<ul className="divide-y">
{options.map((o) => (
<li key={o.id} className="flex items-center justify-between gap-4 px-6 py-4">
<div className="min-w-0">
<div className="flex items-center gap-2">
<div className="font-medium truncate">{o.name}</div>
</div>
{o.description ? (
<div className="text-sm text-muted-foreground truncate">{o.description}</div>
) : null}
</div>
<div className="flex items-center gap-2">
<OptionDialog
mode="edit"
initial={o}
trigger={
<Button variant="secondary" size="sm" className="gap-2">
<Pencil className="h-4 w-4" />
Edit
</Button>
}
/>
<Button
variant="destructive"
size="sm"
className="gap-2"
disabled={busyId === o.id}
onClick={() => onDelete(o.id)}
>
<Trash2 className="h-4 w-4" />
Delete
</Button>
</div>
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
);
}

View File

@ -1,40 +1,67 @@
"use client"
"use client";
import { updateCommissionType } from "@/actions/commissions/types/updateType";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionType, CommissionTypeCustomInput, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma/client";
import MultipleSelector from "@/components/ui/multiselect";
import type {
CommissionCustomInput,
CommissionExtra,
CommissionOption,
CommissionType,
CommissionTypeCustomInput,
CommissionTypeExtra,
CommissionTypeOption,
Tag,
} from "@/generated/prisma/client";
import { commissionTypeSchema } from "@/schemas/commissionType";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod/v4";
import { CommissionCustomInputField } from "./form/CommissionCustomInputField";
import type * as z from "zod/v4";
import { CommissionExtraField } from "./form/CommissionExtraField";
import { CommissionOptionField } from "./form/CommissionOptionField";
type CommissionTypeWithConnections = CommissionType & {
options: (CommissionTypeOption & { option: CommissionOption })[]
extras: (CommissionTypeExtra & { extra: CommissionExtra })[]
customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[]
}
options: (CommissionTypeOption & { option: CommissionOption })[];
extras: (CommissionTypeExtra & { extra: CommissionExtra })[];
customInputs: (CommissionTypeCustomInput & {
customInput: CommissionCustomInput;
})[];
tags: Tag[];
};
type Props = {
type: CommissionTypeWithConnections
allOptions: CommissionOption[],
allExtras: CommissionExtra[],
allCustomInputs: CommissionCustomInput[]
}
type: CommissionTypeWithConnections;
allOptions: CommissionOption[];
allExtras: CommissionExtra[];
allTags: Tag[];
// allCustomInputs: CommissionCustomInput[]
};
export default function EditTypeForm({ type, allOptions, allExtras, allCustomInputs }: Props) {
export default function EditTypeForm({
type,
allOptions,
allExtras,
allTags,
}: Props) {
const router = useRouter();
const form = useForm<z.infer<typeof commissionTypeSchema>>({
resolver: zodResolver(commissionTypeSchema),
defaultValues: {
name: type.name,
description: type.description ?? "",
tagIds: type.tags.map((t) => t.id),
options: type.options.map((o) => ({
optionId: o.optionId,
price: o.price ?? undefined,
@ -55,16 +82,16 @@ export default function EditTypeForm({ type, allOptions, allExtras, allCustomInp
required: f.required,
})),
},
})
});
async function onSubmit(values: z.infer<typeof commissionTypeSchema>) {
try {
await updateCommissionType(type.id, values)
toast.success("Commission type updated.")
router.push("/commissions/types")
await updateCommissionType(type.id, values);
toast.success("Commission type updated.");
router.push("/commissions/types");
} catch (err) {
console.error(err)
toast("Failed to create commission type.")
console.error(err);
toast("Failed to create commission type.");
}
}
@ -81,7 +108,9 @@ export default function EditTypeForm({ type, allOptions, allExtras, allCustomInp
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>The name of the commission type.</FormDescription>
<FormDescription>
The name of the commission type.
</FormDescription>
<FormMessage />
</FormItem>
)}
@ -100,17 +129,58 @@ export default function EditTypeForm({ type, allOptions, allExtras, allCustomInp
</FormItem>
)}
/>
<FormField
control={form.control}
name="tagIds"
render={({ field }) => {
const selectedIds = field.value ?? [];
const selectedOptions = allTags
.filter((t) => selectedIds.includes(t.id))
.map((t) => ({ label: t.name, value: t.id }));
return (
<FormItem>
<FormLabel>Tags</FormLabel>
<FormControl>
<MultipleSelector
options={allTags.map((t) => ({
label: t.name,
value: t.id,
}))}
placeholder="Select tags for this commission type"
hidePlaceholderWhenSelected
selectFirstItem
value={selectedOptions}
onChange={(options) =>
field.onChange(options.map((o) => o.value))
}
/>
</FormControl>
<FormDescription>
Used to link this commission type to tagged artworks.
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<CommissionOptionField options={allOptions} />
<CommissionExtraField extras={allExtras} />
<CommissionCustomInputField customInputs={allCustomInputs} />
{/* <CommissionCustomInputField customInputs={allCustomInputs} /> */}
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
<Button
type="reset"
variant="secondary"
onClick={() => router.back()}
>
Cancel
</Button>
</div>
</form>
</Form>
</div>
);
}
}

View File

@ -5,7 +5,15 @@ import { updateCommissionTypeSortOrder } from "@/actions/commissions/types/updat
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionType, CommissionTypeCustomInput, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma/client";
import {
CommissionCustomInput,
CommissionExtra,
CommissionOption,
CommissionType,
CommissionTypeCustomInput,
CommissionTypeExtra,
CommissionTypeOption,
} from "@/generated/prisma/client";
import {
closestCenter,
DndContext,
@ -187,4 +195,4 @@ export default function ListTypes({ types }: { types: CommissionTypeWithItems[]
</Dialog>
</>
);
}
}

View File

@ -1,47 +1,68 @@
"use client"
"use client";
import { createCommissionType } from "@/actions/commissions/types/newType";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { CommissionCustomInput, CommissionExtra, CommissionOption } from "@/generated/prisma/client";
import MultipleSelector from "@/components/ui/multiselect";
import type {
CommissionCustomInput,
CommissionExtra,
CommissionOption,
Tag,
} from "@/generated/prisma/client";
import { commissionTypeSchema } from "@/schemas/commissionType";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod/v4";
import type * as z from "zod/v4";
import { CommissionCustomInputField } from "./form/CommissionCustomInputField";
import { CommissionExtraField } from "./form/CommissionExtraField";
import { CommissionOptionField } from "./form/CommissionOptionField";
type Props = {
options: CommissionOption[],
extras: CommissionExtra[],
customInputs: CommissionCustomInput[]
}
options: CommissionOption[];
extras: CommissionExtra[];
customInputs: CommissionCustomInput[];
tags: Tag[];
};
export default function NewTypeForm({ options, extras, customInputs }: Props) {
export default function NewTypeForm({
options,
extras,
customInputs,
tags,
}: Props) {
const router = useRouter();
const form = useForm<z.infer<typeof commissionTypeSchema>>({
resolver: zodResolver(commissionTypeSchema),
defaultValues: {
name: "",
description: "",
tagIds: [],
options: [],
extras: [],
},
})
});
async function onSubmit(values: z.infer<typeof commissionTypeSchema>) {
try {
const created = await createCommissionType(values)
console.log("CommissionType created:", created)
toast("Commission type created.")
router.push("/commissions/types")
const created = await createCommissionType(values);
console.log("CommissionType created:", created);
toast("Commission type created.");
router.push("/commissions/types");
} catch (err) {
console.error(err)
toast("Failed to create commission type.")
console.error(err);
toast("Failed to create commission type.");
}
}
@ -58,7 +79,9 @@ export default function NewTypeForm({ options, extras, customInputs }: Props) {
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>The name of the commission type.</FormDescription>
<FormDescription>
The name of the commission type.
</FormDescription>
<FormMessage />
</FormItem>
)}
@ -77,6 +100,41 @@ export default function NewTypeForm({ options, extras, customInputs }: Props) {
</FormItem>
)}
/>
<FormField
control={form.control}
name="tagIds"
render={({ field }) => {
const selectedIds = field.value ?? [];
const selectedOptions = tags
.filter((t) => selectedIds.includes(t.id))
.map((t) => ({ label: t.name, value: t.id }));
return (
<FormItem>
<FormLabel>Tags</FormLabel>
<FormControl>
<MultipleSelector
options={tags.map((t) => ({
label: t.name,
value: t.id,
}))}
placeholder="Select tags for this commission type"
hidePlaceholderWhenSelected
selectFirstItem
value={selectedOptions}
onChange={(options) =>
field.onChange(options.map((o) => o.value))
}
/>
</FormControl>
<FormDescription>
Used to link this commission type to tagged artworks.
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<CommissionOptionField options={options} />
<CommissionExtraField extras={extras} />
@ -84,10 +142,16 @@ export default function NewTypeForm({ options, extras, customInputs }: Props) {
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
<Button
type="reset"
variant="secondary"
onClick={() => router.back()}
>
Cancel
</Button>
</div>
</form>
</Form>
</div>
);
}
}

View File

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

View File

@ -0,0 +1,33 @@
"use client";
import { Menu } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import Sidebar from "./Sidebar";
export default function MobileSidebar() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Open navigation">
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="p-0 w-80">
<SheetHeader className="px-4 py-3">
<SheetTitle>Navigation</SheetTitle>
</SheetHeader>
<Sidebar />
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,122 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { adminNav } from "./nav";
function isActive(pathname: string, href: string) {
if (href === "/") return pathname === "/";
return pathname === href || pathname.startsWith(`${href}/`);
}
export default function AdminSidebar() {
const pathname = usePathname();
return (
<div className="flex h-full flex-col">
{/* Brand / header */}
<div className="flex h-14 items-center px-4">
<Link href="/" className="text-sm font-semibold tracking-tight">
Admin
</Link>
</div>
<Separator />
<ScrollArea className="flex-1">
<nav className="flex flex-col gap-1 p-2">
{adminNav.map((entry) => {
if (entry.type === "link") {
const active = isActive(pathname, entry.href);
return (
<Button
key={entry.href}
asChild
variant={active ? "secondary" : "ghost"}
className={cn(
"justify-start border border-transparent",
"hover:bg-muted/60 dark:hover:bg-muted/40",
active &&
"bg-primary/10 text-primary border-primary/30 dark:bg-primary/20"
)}
>
<Link href={entry.href}>{entry.title}</Link>
</Button>
);
}
// group
const anyChildActive = entry.items.some((i) =>
isActive(pathname, i.href)
);
return (
<Collapsible
key={entry.title}
defaultOpen={anyChildActive}
className="flex flex-col gap-1"
>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className={cn(
"group w-full justify-between border border-transparent",
"hover:bg-muted/60 dark:hover:bg-muted/40",
"data-[state=open]:bg-muted/50 dark:data-[state=open]:bg-muted/30",
anyChildActive && "font-medium text-foreground"
)}
>
<span>{entry.title}</span>
<ChevronDown className="h-4 w-4 transition-transform group-data-[state=open]:rotate-180" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="pl-2">
<div className="flex flex-col gap-1">
{entry.items.map((item) => {
const active = isActive(pathname, item.href);
return (
<Button
key={item.href}
asChild
variant={active ? "secondary" : "ghost"}
size="sm"
className={cn(
"justify-start border border-transparent",
"hover:bg-muted/60 dark:hover:bg-muted/40",
active &&
"bg-primary/10 text-primary border-primary/30 dark:bg-primary/20"
)}
>
<Link href={item.href}>{item.title}</Link>
</Button>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
);
})}
</nav>
</ScrollArea>
<Separator />
{/* Optional: bottom area for version/build info */}
<div className="p-3 text-xs text-muted-foreground">
{/* e.g. v0.1.0 */}
</div>
</div>
);
}

View File

@ -19,6 +19,9 @@ const artworkItems = [
title: "Categories",
href: "/categories",
},
]
const topicItems = [
{
title: "Tags",
href: "/tags",
@ -34,6 +37,10 @@ const commissionItems = [
title: "Types",
href: "/commissions/types",
},
{
title: "Custom Cards",
href: "/commissions/custom-cards",
},
{
title: "Guidelines",
href: "/commissions/guidelines",
@ -106,6 +113,25 @@ export default function TopNav() {
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Topics</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-50 gap-4">
{topicItems.map((item) => (
<li key={item.title}>
<NavigationMenuLink asChild>
<Link href={item.href}>
<div className="text-sm leading-none font-medium">{item.title}</div>
<p className="text-muted-foreground line-clamp-2 text-sm leading-snug">
</p>
</Link>
</NavigationMenuLink>
</li>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Commissions</NavigationMenuTrigger>
<NavigationMenuContent>
@ -203,4 +229,4 @@ export default function TopNav() {
</NavigationMenuList>
</NavigationMenu>
);
}
}

View File

@ -0,0 +1,57 @@
export type AdminNavItem = {
title: string;
href: string;
};
export type AdminNavGroup =
| {
type: "link";
title: string;
href: string;
}
| {
type: "group";
title: string;
items: AdminNavItem[];
};
export const adminNav: AdminNavGroup[] = [
{ type: "link", title: "Home", href: "/" },
{
type: "group",
title: "Uploads",
items: [
{ title: "Single Image", href: "/uploads/single" },
{ title: "Multiple Images", href: "/uploads/bulk" },
],
},
{
type: "group",
title: "Artworks",
items: [
{ title: "Artwork List", href: "/artworks" },
{ title: "Categories", href: "/categories" },
],
},
{
type: "group",
title: "General",
items: [{ title: "Tags", href: "/tags" }],
},
{
type: "group",
title: "Commissions",
items: [
{ title: "Requests", href: "/commissions/requests" },
{ title: "Board", href: "/commissions/kanban" },
{ title: "Types", href: "/commissions/types" },
{ title: "Custom (YCH)", href: "/commissions/custom-cards" },
{ title: "TypeOptions", href: "/commissions/types/options" },
{ title: "TypeExtras", href: "/commissions/types/extras" },
{ title: "Guidelines", href: "/commissions/guidelines" },
],
},
{ type: "link", title: "Terms of Service", href: "/tos" },
{ type: "link", title: "Users", href: "/users" },
];

View File

@ -1,45 +1,74 @@
"use client"
"use client";
import { updateTag } from "@/actions/tags/updateTag";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { ArtCategory, ArtTag, ArtTagAlias } from "@/generated/prisma/client";
import { TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
import type { ArtCategory, Tag, TagAlias } from "@/generated/prisma/client";
import { type TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import MultipleSelector from "../ui/multiselect";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
import { Switch } from "../ui/switch";
import AliasEditor from "./AliasEditor";
export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag & { categories: ArtCategory[], aliases: ArtTagAlias[] }, categories: ArtCategory[], allTags: ArtTag[] }) {
export default function EditTagForm({
tag,
categories,
allTags,
}: {
tag: Tag & {
categoryLinks: { category: ArtCategory }[];
aliases: TagAlias[];
parentId?: string | null;
isParent?: boolean;
showOnAnimalPage?: boolean;
};
categories: ArtCategory[];
allTags: Tag[];
}) {
const router = useRouter();
const form = useForm<TagFormInput>({
resolver: zodResolver(tagSchema),
defaultValues: {
name: tag.name,
description: tag.description || "",
categoryIds: tag.categories?.map(cat => cat.id) ?? [],
parentId: (tag as any).parentId ?? null,
categoryIds: tag.categoryLinks?.map((link) => link.category.id) ?? [],
parentId: tag.parentId ?? null,
isParent: tag.isParent ?? false,
showOnAnimalPage: tag.showOnAnimalPage ?? false,
aliases: tag.aliases?.map(a => a.alias) ?? []
}
})
isVisible: tag.isVisible ?? true,
aliases: tag.aliases?.map((a) => a.alias) ?? [],
},
});
async function onSubmit(values: TagFormInput) {
try {
const updated = await updateTag(tag.id, values)
console.log("Art tag updated:", updated)
toast("Art tag updated.")
router.push("/tags")
const updated = await updateTag(tag.id, values);
console.log("Tag updated:", updated);
toast("Tag updated.");
router.push("/tags");
} catch (err) {
console.error(err)
toast("Failed to update art tag.")
console.error(err);
toast("Failed to update tag.");
}
}
@ -49,6 +78,10 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
return (
<div className="flex flex-col gap-8">
<p className="text-sm text-muted-foreground">
Tags can be used across artworks, commission types, and future miniatures. Category links
are optional and control category-specific behavior.
</p>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* String */}
@ -72,7 +105,10 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} placeholder="A descriptive text (optional)" />
<Textarea
{...field}
placeholder="A descriptive text (optional)"
/>
</FormControl>
<FormMessage />
</FormItem>
@ -83,14 +119,14 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
name="categoryIds"
render={({ field }) => {
const selectedOptions = categories
.filter(cat => field.value?.includes(cat.id))
.map(cat => ({ label: cat.name, value: cat.id }));
.filter((cat) => field.value?.includes(cat.id))
.map((cat) => ({ label: cat.name, value: cat.id }));
return (
<FormItem>
<FormLabel>Categories</FormLabel>
<FormControl>
<MultipleSelector
defaultOptions={categories.map(cat => ({
defaultOptions={categories.map((cat) => ({
label: cat.name,
value: cat.id,
}))}
@ -99,14 +135,14 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
selectFirstItem
value={selectedOptions}
onChange={(options) => {
const ids = options.map(option => option.value);
const ids = options.map((option) => option.value);
field.onChange(ids);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)
);
}}
/>
<FormField
@ -144,7 +180,10 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
<FormItem>
<FormLabel>Aliases</FormLabel>
<FormControl>
<AliasEditor value={field.value ?? []} onChange={field.onChange} />
<AliasEditor
value={field.value ?? []}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -161,7 +200,10 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
<FormDescription></FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
@ -178,7 +220,30 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
<FormDescription></FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex">
<FormField
control={form.control}
name="isVisible"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Visible</FormLabel>
<FormDescription></FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
@ -186,10 +251,16 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
</div>
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
<Button
type="reset"
variant="secondary"
onClick={() => router.back()}
>
Cancel
</Button>
</div>
</form>
</Form>
</div >
</div>
);
}
}

View File

@ -1,23 +1,42 @@
"use client"
"use client";
import { createTag } from "@/actions/tags/createTag";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { ArtCategory, ArtTag } from "@/generated/prisma/client";
import { TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
import type { ArtCategory, Tag } from "@/generated/prisma/client";
import { type TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import MultipleSelector from "../ui/multiselect";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
import { Switch } from "../ui/switch";
import AliasEditor from "./AliasEditor";
export default function NewTagForm({ categories, allTags }: { categories: ArtCategory[], allTags: ArtTag[] }) {
export default function NewTagForm({
categories,
allTags,
}: {
categories: ArtCategory[];
allTags: Tag[];
}) {
const router = useRouter();
const form = useForm<TagFormInput>({
resolver: zodResolver(tagSchema),
@ -28,19 +47,20 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
parentId: null,
isParent: false,
showOnAnimalPage: false,
isVisible: true,
aliases: [],
}
})
},
});
async function onSubmit(values: TagFormInput) {
try {
const created = await createTag(values)
console.log("Art tag created:", created)
toast("Art tag created.")
router.push("/tags")
const created = await createTag(values);
console.log("Tag created:", created);
toast("Tag created.");
router.push("/tags");
} catch (err) {
console.error(err)
toast("Failed to create art tag.")
console.error(err);
toast("Failed to create tag.");
}
}
@ -50,6 +70,11 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
return (
<div className="flex flex-col gap-8">
<p className="text-sm text-muted-foreground">
Tags can be used across artworks, commission types, and future
miniatures. Category links are optional and control category-specific
behavior.
</p>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* String */}
@ -73,7 +98,10 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} placeholder="A descriptive text (optional)" />
<Textarea
{...field}
placeholder="A descriptive text (optional)"
/>
</FormControl>
<FormMessage />
</FormItem>
@ -84,14 +112,14 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
name="categoryIds"
render={({ field }) => {
const selectedOptions = categories
.filter(cat => field.value?.includes(cat.id))
.map(cat => ({ label: cat.name, value: cat.id }));
.filter((cat) => field.value?.includes(cat.id))
.map((cat) => ({ label: cat.name, value: cat.id }));
return (
<FormItem>
<FormLabel>Categories</FormLabel>
<FormControl>
<MultipleSelector
defaultOptions={categories.map(cat => ({
defaultOptions={categories.map((cat) => ({
label: cat.name,
value: cat.id,
}))}
@ -100,14 +128,14 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
selectFirstItem
value={selectedOptions}
onChange={(options) => {
const ids = options.map(option => option.value);
const ids = options.map((option) => option.value);
field.onChange(ids);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)
);
}}
/>
<FormField
@ -145,7 +173,10 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
<FormItem>
<FormLabel>Aliases</FormLabel>
<FormControl>
<AliasEditor value={field.value ?? []} onChange={field.onChange} />
<AliasEditor
value={field.value ?? []}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -162,7 +193,10 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
<FormDescription></FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
@ -179,7 +213,30 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
<FormDescription></FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex">
<FormField
control={form.control}
name="isVisible"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Visible</FormLabel>
<FormDescription></FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
@ -187,10 +244,16 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
</div>
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
<Button
type="reset"
variant="secondary"
onClick={() => router.back()}
>
Cancel
</Button>
</div>
</form>
</Form>
</div >
</div>
);
}
}

View File

@ -0,0 +1,33 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

1106
src/components/ui/kanban.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@ -142,15 +142,6 @@ function removePickedOption(groupOption: GroupOption, picked: Option[]) {
return cloneOption;
}
function isOptionsExist(groupOption: GroupOption, targetOption: Option[]) {
for (const [, value] of Object.entries(groupOption)) {
if (value.some((option) => targetOption.find((p) => p.value === option.value))) {
return true;
}
}
return false;
}
function normalizeInput(s: string) {
return s.trim().replace(/\s+/g, " ");
}
@ -238,7 +229,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
[selected],
);
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
const handleClickOutside = React.useCallback((event: MouseEvent | TouchEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
@ -248,7 +239,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
setOpen(false);
inputRef.current.blur();
}
};
}, []);
const handleUnselect = React.useCallback(
(option: Option) => {
@ -294,7 +285,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchend', handleClickOutside);
};
}, [open]);
}, [open, handleClickOutside]);
useEffect(() => {
if (value) {
@ -311,7 +302,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
if (JSON.stringify(newOption) !== JSON.stringify(options)) {
setOptions(newOption);
}
}, [arrayDefaultOptions, arrayOptions, groupBy, onSearch, options]);
}, [arrayOptions, groupBy, onSearch, options]);
useEffect(() => {
/** sync search */
@ -334,8 +325,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
};
void exec();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]);
}, [debouncedSearchTerm, groupBy, onSearchSync, open, triggerSearchOnFocus]);
useEffect(() => {
/** async search */
@ -360,8 +350,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
};
void exec();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]);
}, [debouncedSearchTerm, groupBy, onSearch, open, triggerSearchOnFocus]);
const CreatableItem = () => {
if (!creatable) return undefined;
@ -448,14 +437,10 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
return commandProps.filter;
}
if (creatable) {
return (value: string, search: string) => {
return value.toLowerCase().includes(search.toLowerCase()) ? 1 : -1;
};
}
// Using default filter in `cmdk`. We don't have to provide it.
return undefined;
}, [creatable, commandProps?.filter]);
return (value: string, search: string) => {
return value.toLowerCase().includes(search.toLowerCase()) ? 1 : -1;
};
}, [commandProps?.filter]);
const orderedGroupEntries = React.useMemo(() => {
const entries = Object.entries(selectables);
@ -504,10 +489,6 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
},
className,
)}
onClick={() => {
if (disabled) return;
inputRef?.current?.focus();
}}
>
<div className="relative flex flex-wrap gap-1">
{selected.map((option) => {
@ -610,7 +591,10 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
}}
>
{isLoading ? (
<>{loadingIndicator}</>
// biome-ignore lint: lint/complexity/noUselessFragments
<>
{loadingIndicator}
</>
) : (
<>
{EmptyItem()}
@ -618,47 +602,45 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
{!selectFirstItem && <CommandItem value="-" className="hidden" />}
{orderedGroupEntries.map(([key, dropdowns]) => (
<CommandGroup key={key} heading={key} className="h-full overflow-auto">
<>
{dropdowns.map((option) => {
const alreadySelected = selected.some((s) => s.value === option.value);
const disabledItem = option.disable || (showSelectedInDropdown && alreadySelected);
{dropdowns.map((option) => {
const alreadySelected = selected.some((s) => s.value === option.value);
const disabledItem = option.disable || (showSelectedInDropdown && alreadySelected);
return (
<CommandItem
key={option.value}
value={option.value}
disabled={disabledItem}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={() => {
if (disabledItem) return;
return (
<CommandItem
key={option.value}
value={`${option.label}::${option.value}`}
disabled={disabledItem}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={() => {
if (disabledItem) return;
if (selected.length >= maxSelected) {
onMaxSelected?.(selected.length);
return;
}
if (selected.length >= maxSelected) {
onMaxSelected?.(selected.length);
return;
}
setInputValue('');
setInputValue('');
// Guard against duplicates (safety)
if (selected.some((s) => s.value === option.value)) return;
// Guard against duplicates (safety)
if (selected.some((s) => s.value === option.value)) return;
const newOptions = [...selected, option];
setSelected(newOptions);
onChange?.(newOptions);
}}
className={cn(
'cursor-pointer',
disabledItem && 'cursor-default text-muted-foreground',
)}
>
{option.label}
</CommandItem>
);
})}
</>
const newOptions = [...selected, option];
setSelected(newOptions);
onChange?.(newOptions);
}}
className={cn(
'cursor-pointer',
disabledItem && 'cursor-default text-muted-foreground',
)}
>
{option.label}
</CommandItem>
);
})}
</CommandGroup>
))}
</>

View File

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@ -1,27 +1,27 @@
"use client"
"use client";
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
@ -30,7 +30,7 @@ function SelectTrigger({
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
@ -38,7 +38,7 @@ function SelectTrigger({
data-size={size}
className={cn(
"border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]: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 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
>
@ -47,7 +47,7 @@ function SelectTrigger({
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
);
}
function SelectContent({
@ -65,7 +65,7 @@ function SelectContent({
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
align={align}
@ -76,7 +76,7 @@ function SelectContent({
className={cn(
"p-1",
position === "popper" &&
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1"
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1",
)}
>
{children}
@ -84,7 +84,7 @@ function SelectContent({
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
);
}
function SelectLabel({
@ -97,7 +97,7 @@ function SelectLabel({
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
);
}
function SelectItem({
@ -110,7 +110,7 @@ function SelectItem({
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
className,
)}
{...props}
>
@ -124,7 +124,7 @@ function SelectItem({
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
}
function SelectSeparator({
@ -137,7 +137,7 @@ function SelectSeparator({
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
);
}
function SelectScrollUpButton({
@ -149,13 +149,13 @@ function SelectScrollUpButton({
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
);
}
function SelectScrollDownButton({
@ -167,13 +167,13 @@ function SelectScrollDownButton({
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
);
}
export {
@ -187,5 +187,5 @@ export {
SelectSeparator,
SelectTrigger,
SelectValue
}
};

View File

@ -0,0 +1,44 @@
export const COMMISSION_STATUSES = [
"NEW",
"REVIEWING",
"ACCEPTED",
"REJECTED",
"INPROGRESS",
"COMPLETED",
"SPAM",
] as const;
export type CommissionStatus = (typeof COMMISSION_STATUSES)[number];
export const BOARD_COLUMNS = {
intake: {
title: "Intake",
statuses: ["NEW", "REVIEWING", "ACCEPTED"] as const,
// when you drop into this column, we normalize to one canonical status:
// NEW should usually be system-created; for manual moves, REVIEWING is safer.
canonicalStatus: "REVIEWING" as const,
},
inProgress: {
title: "In Progress",
statuses: ["INPROGRESS"] as const,
canonicalStatus: "INPROGRESS" as const,
},
completed: {
title: "Completed",
statuses: ["COMPLETED"] as const,
canonicalStatus: "COMPLETED" as const,
},
} as const;
export type BoardColumnId = keyof typeof BOARD_COLUMNS;
export function columnIdForStatus(status: string): BoardColumnId | null {
if (BOARD_COLUMNS.intake.statuses.includes(status as any)) return "intake";
if (BOARD_COLUMNS.inProgress.statuses.includes(status as any)) return "inProgress";
if (BOARD_COLUMNS.completed.statuses.includes(status as any)) return "completed";
return null;
}
export function canonicalStatusForColumn(col: BoardColumnId): CommissionStatus {
return BOARD_COLUMNS[col].canonicalStatus as CommissionStatus;
}

62
src/lib/compose-refs.ts Normal file
View File

@ -0,0 +1,62 @@
import * as React from "react";
type PossibleRef<T> = React.Ref<T> | undefined;
/**
* Set a given ref to a given value
* This utility takes care of different types of refs: callback refs and RefObject(s)
*/
function setRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === "function") {
return ref(value);
}
if (ref !== null && ref !== undefined) {
ref.current = value;
}
}
/**
* A utility to compose multiple refs together
* Accepts callback refs and RefObject(s)
*/
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
return (node) => {
let hasCleanup = false;
const cleanups = refs.map((ref) => {
const cleanup = setRef(ref, node);
if (!hasCleanup && typeof cleanup === "function") {
hasCleanup = true;
}
return cleanup;
});
// React <19 will log an error to the console if a callback ref returns a
// value. We don't use ref cleanups internally so this will only happen if a
// user's ref callback returns a value, which we only expect if they are
// using the cleanup functionality added in React 19.
if (hasCleanup) {
return () => {
for (let i = 0; i < cleanups.length; i++) {
const cleanup = cleanups[i];
if (typeof cleanup === "function") {
cleanup();
} else {
setRef(refs[i], null);
}
}
};
}
};
}
/**
* A custom hook that composes multiple refs
* Accepts callback refs and RefObject(s)
*/
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
// biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values
return React.useCallback(composeRefs(...refs), refs);
}
export { composeRefs, useComposedRefs };

View File

@ -7,6 +7,7 @@ export const tagSchema = z.object({
parentId: z.string().nullable().optional(),
isParent: z.boolean(),
showOnAnimalPage: z.boolean(),
isVisible: z.boolean().default(true),
aliases: z
.array(z.string().trim().min(1))
@ -19,4 +20,3 @@ export const tagSchema = z.object({
export type TagFormInput = z.input<typeof tagSchema>;
export type TagFormOutput = z.output<typeof tagSchema>;

View File

@ -0,0 +1,36 @@
import * as z from "zod/v4";
const rangePattern = /^\d{1,3}\d{1,3}$/;
const optionField = z.object({
optionId: z.string(),
price: z.number().optional(),
pricePercent: z.number().optional(),
priceRange: z
.string()
.regex(rangePattern, "Format must be like '1080'")
.optional(),
});
const extraField = z.object({
extraId: z.string(),
price: z.number().optional(),
pricePercent: z.number().optional(),
priceRange: z
.string()
.regex(rangePattern, "Format must be like '1080'")
.optional(),
});
export const commissionCustomCardSchema = z.object({
name: z.string().min(1, "Name is required. Min 1 character."),
description: z.string().optional(),
isVisible: z.boolean(),
isSpecialOffer: z.boolean(),
referenceImageUrl: z.string().nullable().optional(),
tagIds: z.array(z.string()).optional(),
options: z.array(optionField).optional(),
extras: z.array(extraField).optional(),
});
export type CommissionCustomCardValues = z.infer<typeof commissionCustomCardSchema>;

View File

@ -6,14 +6,20 @@ const optionField = z.object({
optionId: z.string(),
price: z.number().optional(),
pricePercent: z.number().optional(),
priceRange: z.string().regex(rangePattern, "Format must be like '1080'").optional(),
priceRange: z
.string()
.regex(rangePattern, "Format must be like '1080'")
.optional(),
});
const extraField = z.object({
extraId: z.string(),
price: z.number().optional(),
pricePercent: z.number().optional(),
priceRange: z.string().regex(rangePattern, "Format must be like '1080'").optional(),
priceRange: z
.string()
.regex(rangePattern, "Format must be like '1080'")
.optional(),
});
const customInputsField = z.object({
@ -26,9 +32,24 @@ const customInputsField = z.object({
export const commissionTypeSchema = z.object({
name: z.string().min(1, "Name is required. Min 1 character."),
description: z.string().optional(),
tagIds: z.array(z.string()).optional(),
options: z.array(optionField).optional(),
extras: z.array(extraField).optional(),
customInputs: z.array(customInputsField).optional(),
})
});
export type commissionTypeSchema = z.infer<typeof commissionTypeSchema>
export type commissionTypeSchema = z.infer<typeof commissionTypeSchema>;
export const commissionOptionSchema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string().optional(),
});
export type CommissionOptionValues = z.infer<typeof commissionOptionSchema>;
export const commissionExtraSchema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string().optional(),
});
export type CommissionExtraValues = z.infer<typeof commissionExtraSchema>;

View File

@ -1,4 +1,4 @@
import { Prisma } from "@/generated/prisma/client";
import type { Prisma } from "@/generated/prisma/client";
export type ArtworkWithRelations = Prisma.ArtworkGetPayload<{
include: {
@ -10,9 +10,10 @@ export type ArtworkWithRelations = Prisma.ArtworkGetPayload<{
colors: true;
tags: true;
variants: true;
timelapse: true;
};
}>;
export type CategoryWithTags = Prisma.ArtCategoryGetPayload<{
include: { tags: true };
}>;
include: { tagLinks: { include: { tag: true } } };
}>;

Some files were not shown because too many files have changed in this diff Show More