Add image upload and edit functions
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -39,3 +39,5 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
/src/generated/prisma
|
||||||
|
|||||||
968
bun.lock
Normal file
968
bun.lock
Normal file
@ -0,0 +1,968 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "admin.gaertan.art",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.954.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
|
"@prisma/client": "^7.2.0",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
"@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-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"culori": "^4.0.2",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"lucide-react": "^0.561.0",
|
||||||
|
"next": "16.0.10",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"node-vibrant": "^4.0.3",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"react": "19.2.1",
|
||||||
|
"react-day-picker": "^9.13.0",
|
||||||
|
"react-dom": "19.2.1",
|
||||||
|
"react-hook-form": "^7.69.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
|
"zod": "^4.2.1",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "2.2.0",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/culori": "^4.0.1",
|
||||||
|
"@types/date-fns": "^2.6.3",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"@types/uuid": "^11.0.0",
|
||||||
|
"prisma": "^7.2.0",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
|
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||||
|
|
||||||
|
"@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="],
|
||||||
|
|
||||||
|
"@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="],
|
||||||
|
|
||||||
|
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
|
||||||
|
|
||||||
|
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
|
||||||
|
|
||||||
|
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
|
||||||
|
|
||||||
|
"@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.954.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.954.0", "@aws-sdk/credential-provider-node": "3.954.0", "@aws-sdk/middleware-bucket-endpoint": "3.953.0", "@aws-sdk/middleware-expect-continue": "3.953.0", "@aws-sdk/middleware-flexible-checksums": "3.954.0", "@aws-sdk/middleware-host-header": "3.953.0", "@aws-sdk/middleware-location-constraint": "3.953.0", "@aws-sdk/middleware-logger": "3.953.0", "@aws-sdk/middleware-recursion-detection": "3.953.0", "@aws-sdk/middleware-sdk-s3": "3.954.0", "@aws-sdk/middleware-ssec": "3.953.0", "@aws-sdk/middleware-user-agent": "3.954.0", "@aws-sdk/region-config-resolver": "3.953.0", "@aws-sdk/signature-v4-multi-region": "3.954.0", "@aws-sdk/types": "3.953.0", "@aws-sdk/util-endpoints": "3.953.0", "@aws-sdk/util-user-agent-browser": "3.953.0", "@aws-sdk/util-user-agent-node": "3.954.0", "@smithy/config-resolver": "^4.4.4", "@smithy/core": "^3.19.0", "@smithy/eventstream-serde-browser": "^4.2.6", "@smithy/eventstream-serde-config-resolver": "^4.3.6", "@smithy/eventstream-serde-node": "^4.2.6", "@smithy/fetch-http-handler": "^5.3.7", "@smithy/hash-blob-browser": "^4.2.7", "@smithy/hash-node": "^4.2.6", "@smithy/hash-stream-node": "^4.2.6", "@smithy/invalid-dependency": "^4.2.6", "@smithy/md5-js": "^4.2.6", "@smithy/middleware-content-length": "^4.2.6", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-retry": "^4.4.16", "@smithy/middleware-serde": "^4.2.7", "@smithy/middleware-stack": "^4.2.6", "@smithy/node-config-provider": "^4.3.6", "@smithy/node-http-handler": "^4.4.6", "@smithy/protocol-http": "^5.3.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@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.15", "@smithy/util-defaults-mode-node": "^4.2.18", "@smithy/util-endpoints": "^3.2.6", "@smithy/util-middleware": "^4.2.6", "@smithy/util-retry": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-DoeySsljzjuWRzqoETLszHGKKOOWlzuGZh3oAF7TkYRsrwbuYYmttrWomb9koogaF0S5YSPwCMCUbKbpF0lbTA=="],
|
||||||
|
|
||||||
|
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.954.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.954.0", "@aws-sdk/middleware-host-header": "3.953.0", "@aws-sdk/middleware-logger": "3.953.0", "@aws-sdk/middleware-recursion-detection": "3.953.0", "@aws-sdk/middleware-user-agent": "3.954.0", "@aws-sdk/region-config-resolver": "3.953.0", "@aws-sdk/types": "3.953.0", "@aws-sdk/util-endpoints": "3.953.0", "@aws-sdk/util-user-agent-browser": "3.953.0", "@aws-sdk/util-user-agent-node": "3.954.0", "@smithy/config-resolver": "^4.4.4", "@smithy/core": "^3.19.0", "@smithy/fetch-http-handler": "^5.3.7", "@smithy/hash-node": "^4.2.6", "@smithy/invalid-dependency": "^4.2.6", "@smithy/middleware-content-length": "^4.2.6", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-retry": "^4.4.16", "@smithy/middleware-serde": "^4.2.7", "@smithy/middleware-stack": "^4.2.6", "@smithy/node-config-provider": "^4.3.6", "@smithy/node-http-handler": "^4.4.6", "@smithy/protocol-http": "^5.3.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@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.15", "@smithy/util-defaults-mode-node": "^4.2.18", "@smithy/util-endpoints": "^3.2.6", "@smithy/util-middleware": "^4.2.6", "@smithy/util-retry": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-FVyMAvlFhLK68DHWB1lSkCRTm25xl38bIZDd+jKt5+yDolCrG5+n9aIN8AA8jNO1HNGhZuMjSIQm9r5rGmJH8g=="],
|
||||||
|
|
||||||
|
"@aws-sdk/core": ["@aws-sdk/core@3.954.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@aws-sdk/xml-builder": "3.953.0", "@smithy/core": "^3.19.0", "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/signature-v4": "^5.3.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-5oYO5RP+mvCNXNj8XnF9jZo0EP0LTseYOJVNQYcii1D9DJqzHL3HJWurYh7cXxz7G7eDyvVYA01O9Xpt34TdoA=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.954.0", "", { "dependencies": { "@aws-sdk/core": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/property-provider": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-2HNkqBjfsvyoRuPAiFh86JBFMFyaCNhL4VyH6XqwTGKZffjG7hdBmzXPy7AT7G3oFh1k/1Zc27v0qxaKoK7mBA=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.954.0", "", { "dependencies": { "@aws-sdk/core": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/property-provider": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" } }, "sha512-CrWD5300+NE1OYRnSVDxoG7G0b5cLIZb7yp+rNQ5Jq/kqnTmyJXpVAsivq+bQIDaGzPXhadzpAMIoo7K/aHaag=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.954.0", "", { "dependencies": { "@aws-sdk/core": "3.954.0", "@aws-sdk/credential-provider-env": "3.954.0", "@aws-sdk/credential-provider-http": "3.954.0", "@aws-sdk/credential-provider-login": "3.954.0", "@aws-sdk/credential-provider-process": "3.954.0", "@aws-sdk/credential-provider-sso": "3.954.0", "@aws-sdk/credential-provider-web-identity": "3.954.0", "@aws-sdk/nested-clients": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/credential-provider-imds": "^4.2.6", "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-WAFD8pVwRSoBsuXcoD+s/hrdsP9Z0PNUedSgkOGExuJVAabpM2cIIMzYNsdHio9XFZUSqHkv8mF5mQXuIZvuzg=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.954.0", "", { "dependencies": { "@aws-sdk/core": "3.954.0", "@aws-sdk/nested-clients": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/property-provider": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-EYqaBWwdVbVK7prmsmgTWLPptoWREplPkFMFscOpVmseDvf/0IjYNbNLLtfuhy/6L7ZBGI9wat2k4u0MRivvxA=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.954.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.954.0", "@aws-sdk/credential-provider-http": "3.954.0", "@aws-sdk/credential-provider-ini": "3.954.0", "@aws-sdk/credential-provider-process": "3.954.0", "@aws-sdk/credential-provider-sso": "3.954.0", "@aws-sdk/credential-provider-web-identity": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/credential-provider-imds": "^4.2.6", "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-UPBjw7Lnly5i+/rES8Z5U+nPaumzEUYOE/wrHkxyH6JjwFWn8w7R07fE5Z5cgYlIq1U1lQ7sxYwB3wHPpQ65Aw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.954.0", "", { "dependencies": { "@aws-sdk/core": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Y1/0O2LgbKM8iIgcVj/GNEQW6p90LVTCOzF2CI1pouoKqxmZ/1F7F66WHoa6XUOfKaCRj/R6nuMR3om9ThaM5A=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.954.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.954.0", "@aws-sdk/core": "3.954.0", "@aws-sdk/token-providers": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-UXxGfkp/plFRdyidMLvNul5zoLKmHhVQOCrD2OgR/lg9jNqNmJ7abF+Qu8abo902iDkhU21Qj4M398cx6l8Kng=="],
|
||||||
|
|
||||||
|
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.954.0", "", { "dependencies": { "@aws-sdk/core": "3.954.0", "@aws-sdk/nested-clients": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-XEyf1T08q1tG4zkTS4Dnf1cAQyrJUo/xlvi6XNpqGhY3bOmKUYE2h/K6eITIdytDL9VuCpWYQ6YRcIVtL29E0w=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@aws-sdk/util-arn-parser": "3.953.0", "@smithy/node-config-provider": "^4.3.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-YHVRIOowtGIl/L2WuS83FgRlm31tU0aL1yryWaFtF+AFjA5BIeiFkxIZqaRGxJpJvFEBdohsyq6Ipv5mgWfezg=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-BQTVXrypQ0rbb7au/Hk4IS5GaJZlwk6O44Rjk6Kxb0IvGQhSurNTuesFiJx1sLbf+w+T31saPtODcfQQERqhCQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.954.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-hHOPDJyxucNodkgapLhA0VdwDBwVYN9DX20aA6j+3nwutAlZ5skaV7Bw0W3YC7Fh/ieDKKhcSZulONd4lVTwMg=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-jTGhfkONav+r4E6HLOrl5SzBqDmPByUYCkyB/c/3TVb8jX3wAZx8/q9bphKpCh+G5ARi3IdbSisgkZrJYqQ19Q=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-h0urrbteIQEybyIISaJfQLZ/+/lJPRzPWAQT4epvzfgv/4MKZI7K83dK7SfTwAooVKFBHiCMok2Cf0iHDt07Kw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PlWdVYgcuptkIC0ZKqVUhWNtSHXJSx7U9V8J7dJjRmsXC40X7zpEycvrkzDMJjeTDGcCceYbyYAg/4X1lkcIMw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-cmIJx0gWeesUKK4YwgE+VQL3mpACr3/J24fbwnc1Z5tntC86b+HQFzU5vsBDw6lLwyD46dBgWdsXFh1jL+ZaFw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.954.0", "", { "dependencies": { "@aws-sdk/core": "3.954.0", "@aws-sdk/types": "3.953.0", "@aws-sdk/util-arn-parser": "3.953.0", "@smithy/core": "^3.19.0", "@smithy/node-config-provider": "^4.3.6", "@smithy/protocol-http": "^5.3.6", "@smithy/signature-v4": "^5.3.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-274CNmnRjknmfFb2o0Azxic54fnujaA8AYSeRUOho3lN48TVzx85eAFWj2kLgvUJO88pE3jBDPWboKQiQdXeUQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-OrhG1kcQ9zZh3NS3RovR028N0+UndQ957zF1k5HPLeFLwFwQN1uPOufzzPzAyXIIKtR69ARFsQI4mstZS4DMvw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.954.0", "", { "dependencies": { "@aws-sdk/core": "3.954.0", "@aws-sdk/types": "3.953.0", "@aws-sdk/util-endpoints": "3.953.0", "@smithy/core": "^3.19.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-5PX8JDe3dB2+MqXeGIhmgFnm2rbVsSxhz+Xyuu1oxLtbOn+a9UDA+sNBufEBjt3UxWy5qwEEY1fxdbXXayjlGg=="],
|
||||||
|
|
||||||
|
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.954.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.954.0", "@aws-sdk/middleware-host-header": "3.953.0", "@aws-sdk/middleware-logger": "3.953.0", "@aws-sdk/middleware-recursion-detection": "3.953.0", "@aws-sdk/middleware-user-agent": "3.954.0", "@aws-sdk/region-config-resolver": "3.953.0", "@aws-sdk/types": "3.953.0", "@aws-sdk/util-endpoints": "3.953.0", "@aws-sdk/util-user-agent-browser": "3.953.0", "@aws-sdk/util-user-agent-node": "3.954.0", "@smithy/config-resolver": "^4.4.4", "@smithy/core": "^3.19.0", "@smithy/fetch-http-handler": "^5.3.7", "@smithy/hash-node": "^4.2.6", "@smithy/invalid-dependency": "^4.2.6", "@smithy/middleware-content-length": "^4.2.6", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-retry": "^4.4.16", "@smithy/middleware-serde": "^4.2.7", "@smithy/middleware-stack": "^4.2.6", "@smithy/node-config-provider": "^4.3.6", "@smithy/node-http-handler": "^4.4.6", "@smithy/protocol-http": "^5.3.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@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.15", "@smithy/util-defaults-mode-node": "^4.2.18", "@smithy/util-endpoints": "^3.2.6", "@smithy/util-middleware": "^4.2.6", "@smithy/util-retry": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-JLUhf35fTQIDPLk6G5KPggL9tV//Hjhy6+N2zZeis76LuBRNhKDq8z1CFyKhjf00vXi/tDYdn9D7y9emI+5Y/g=="],
|
||||||
|
|
||||||
|
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@smithy/config-resolver": "^4.4.4", "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-5MJgnsc+HLO+le0EK1cy92yrC7kyhGZSpaq8PcQvKs9qtXCXT5Tb6tMdkr5Y07JxYsYOV1omWBynvL6PWh08tQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.954.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.954.0", "@aws-sdk/types": "3.953.0", "@aws-sdk/util-format-url": "3.953.0", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/protocol-http": "^5.3.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-ypFOt8JK/x6p2hD9Jd5ctStw8h32KdgSBV+eXAMvcMP4DFt7wp6f7StwbjAblehZnPwNolFJE4Hz2F08nh4VdA=="],
|
||||||
|
|
||||||
|
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.954.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/protocol-http": "^5.3.6", "@smithy/signature-v4": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-GJJbUaSlGrMSRWui3Oz8ByygpQlzDGm195yTKirgGyu4tfYrFr/QWrWT42EUktY/L4Irev1pdHTuLS+AGHO1gw=="],
|
||||||
|
|
||||||
|
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.954.0", "", { "dependencies": { "@aws-sdk/core": "3.954.0", "@aws-sdk/nested-clients": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-rDyN3oQQKMOJgyQ9/LNbh4fAGAj8ePMGOAQzSP/kyzizmViI6STpBW1o/VRqiTgMNi1bvA9ZasDtfrJqcVt0iA=="],
|
||||||
|
|
||||||
|
"@aws-sdk/types": ["@aws-sdk/types@3.953.0", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g=="],
|
||||||
|
|
||||||
|
"@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.953.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9hqdKkn4OvYzzaLryq2xnwcrPc8ziY34i9szUdgBfSqEC6pBxbY9/lLXmrgzfwMSL2Z7/v2go4Od0p5eukKLMQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@smithy/util-endpoints": "^3.2.6", "tslib": "^2.6.2" } }, "sha512-rjaS6jrFksopXvNg6YeN+D1lYwhcByORNlFuYesFvaQNtPOufbE5tJL4GJ3TMXyaY0uFR28N5BHHITPyWWfH/g=="],
|
||||||
|
|
||||||
|
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-fs70vtTiBhp/T9ss52OuW2LGJqPoNBbd1+wxqh82CMdzkOvCzI3qa/cK8tR0jCFeIjGeiV74lAskImRxu/V4lg=="],
|
||||||
|
|
||||||
|
"@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.953.0", "", { "dependencies": { "@aws-sdk/types": "3.953.0", "@smithy/types": "^4.10.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UF5NeqYesWuFao+u7LJvpV1SJCaLml5BtFZKUdTnNNMeN6jvV+dW/eQoFGpXF94RCqguX0XESmRuRRPQp+/rzQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.954.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.954.0", "@aws-sdk/types": "3.953.0", "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-fB5S5VOu7OFkeNzcblQlez4AjO5hgDFaa7phYt7716YWisY3RjAaQPlxgv+G3GltHHDJIfzEC5aRxdf62B9zMg=="],
|
||||||
|
|
||||||
|
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.953.0", "", { "dependencies": { "@smithy/types": "^4.10.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-Zmrj21jQ2OeOJGr9spPiN00aQvXa/WUqRXcTVENhrMt+OFoSOfDFpYhUj9NQ09QmQ8KMWFoWuWW6iKurNqLvAA=="],
|
||||||
|
|
||||||
|
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.2", "", {}, "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg=="],
|
||||||
|
|
||||||
|
"@biomejs/biome": ["@biomejs/biome@2.2.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.0", "@biomejs/cli-darwin-x64": "2.2.0", "@biomejs/cli-linux-arm64": "2.2.0", "@biomejs/cli-linux-arm64-musl": "2.2.0", "@biomejs/cli-linux-x64": "2.2.0", "@biomejs/cli-linux-x64-musl": "2.2.0", "@biomejs/cli-win32-arm64": "2.2.0", "@biomejs/cli-win32-x64": "2.2.0" }, "bin": { "biome": "bin/biome" } }, "sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww=="],
|
||||||
|
|
||||||
|
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="],
|
||||||
|
|
||||||
|
"@chevrotain/gast": ["@chevrotain/gast@10.5.0", "", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="],
|
||||||
|
|
||||||
|
"@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="],
|
||||||
|
|
||||||
|
"@chevrotain/utils": ["@chevrotain/utils@10.5.0", "", {}, "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="],
|
||||||
|
|
||||||
|
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
|
||||||
|
|
||||||
|
"@electric-sql/pglite": ["@electric-sql/pglite@0.3.2", "", {}, "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w=="],
|
||||||
|
|
||||||
|
"@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-tools": ["@electric-sql/pglite-tools@0.2.7", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.2" } }, "sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||||
|
|
||||||
|
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||||
|
|
||||||
|
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
||||||
|
|
||||||
|
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||||
|
|
||||||
|
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||||
|
|
||||||
|
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||||
|
|
||||||
|
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||||
|
|
||||||
|
"@jimp/bmp": ["@jimp/bmp@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "bmp-js": "^0.1.0" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g=="],
|
||||||
|
|
||||||
|
"@jimp/core": ["@jimp/core@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "any-base": "^1.1.0", "buffer": "^5.2.0", "exif-parser": "^0.1.12", "file-type": "^16.5.4", "isomorphic-fetch": "^3.0.0", "pixelmatch": "^4.0.2", "tinycolor2": "^1.6.0" } }, "sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA=="],
|
||||||
|
|
||||||
|
"@jimp/custom": ["@jimp/custom@0.22.12", "", { "dependencies": { "@jimp/core": "^0.22.12" } }, "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q=="],
|
||||||
|
|
||||||
|
"@jimp/gif": ["@jimp/gif@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "gifwrap": "^0.10.1", "omggif": "^1.0.9" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg=="],
|
||||||
|
|
||||||
|
"@jimp/jpeg": ["@jimp/jpeg@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "jpeg-js": "^0.4.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-resize": ["@jimp/plugin-resize@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg=="],
|
||||||
|
|
||||||
|
"@jimp/png": ["@jimp/png@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "pngjs": "^6.0.0" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg=="],
|
||||||
|
|
||||||
|
"@jimp/tiff": ["@jimp/tiff@0.22.12", "", { "dependencies": { "utif2": "^4.0.1" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg=="],
|
||||||
|
|
||||||
|
"@jimp/types": ["@jimp/types@0.22.12", "", { "dependencies": { "@jimp/bmp": "^0.22.12", "@jimp/gif": "^0.22.12", "@jimp/jpeg": "^0.22.12", "@jimp/png": "^0.22.12", "@jimp/tiff": "^0.22.12", "timm": "^1.6.1" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA=="],
|
||||||
|
|
||||||
|
"@jimp/utils": ["@jimp/utils@0.22.12", "", { "dependencies": { "regenerator-runtime": "^0.13.3" } }, "sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.12.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w=="],
|
||||||
|
|
||||||
|
"@next/env": ["@next/env@16.0.10", "", {}, "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang=="],
|
||||||
|
|
||||||
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg=="],
|
||||||
|
|
||||||
|
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.10", "", { "os": "linux", "cpu": "x64" }, "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.10", "", { "os": "linux", "cpu": "x64" }, "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g=="],
|
||||||
|
|
||||||
|
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg=="],
|
||||||
|
|
||||||
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.10", "", { "os": "win32", "cpu": "x64" }, "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q=="],
|
||||||
|
|
||||||
|
"@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/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-runtime-utils": ["@prisma/client-runtime-utils@7.2.0", "", {}, "sha512-dn7oB53v0tqkB0wBdMuTNFNPdEbfICEUe82Tn9FoKAhJCUkDH+fmyEp0ClciGh+9Hp2Tuu2K52kth2MTLstvmA=="],
|
||||||
|
|
||||||
|
"@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/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="],
|
||||||
|
|
||||||
|
"@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/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-gzrUcbI9VmHS24Uf+0+7DNzdIw7keglJsD5m/MHxQOU68OhGVzlphQRobLiDMn8CHNA2XN8uugwKjudVtnfMVQ=="],
|
||||||
|
|
||||||
|
"@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-version": ["@prisma/engines-version@7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", "", {}, "sha512-KezsjCZDsbjNR7SzIiVlUsn9PnLePI7r5uxABlwL+xoerurZTfgQVbIjvjF2sVr3Uc0ZcsnREw3F84HvbggGdA=="],
|
||||||
|
|
||||||
|
"@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/get-platform": ["@prisma/get-platform@6.8.2", "", { "dependencies": { "@prisma/debug": "6.8.2" } }, "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow=="],
|
||||||
|
|
||||||
|
"@prisma/query-plan-executor": ["@prisma/query-plan-executor@6.18.0", "", {}, "sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.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-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "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-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-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "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-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "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-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "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-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@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-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "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-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-label": ["@radix-ui/react-label@2.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-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "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-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-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "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-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "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-dismissable-layer": "1.1.11", "@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-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" }, "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-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "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-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-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "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-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@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-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "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-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@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-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.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-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@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-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@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-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-slot": ["@radix-ui/react-slot@1.2.4", "", { "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-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "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-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-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.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-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
|
||||||
|
|
||||||
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||||
|
|
||||||
|
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-P7JD4J+wxHMpGxqIg6SHno2tPkZbBUBLbPpR5/T1DEUvw/mEaINBMaPFZNM7lA+ToSCZ36j6nMHa+5kej+fhGg=="],
|
||||||
|
|
||||||
|
"@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.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-s3U5ChS21DwU54kMmZ0UJumoS5cg0+rGVZvN6f5Lp6EbAVi0ZyP+qDSHdewfmXKUgNK1j3z45JyzulkDukrjAA=="],
|
||||||
|
|
||||||
|
"@smithy/core": ["@smithy/core@3.19.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.7", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w=="],
|
||||||
|
|
||||||
|
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-xBmawExyTzOjbhzkZwg+vVm/khg28kG+rj2sbGlULjFd1jI70sv/cbpaR0Ev4Yfd6CpDUDRMe64cTqR//wAOyA=="],
|
||||||
|
|
||||||
|
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-OZfsI+YRG26XZik/jKMMg37acnBSbUiK/8nETW3uM3mLj+0tMmFXdHQw1e5WEd/IHN8BGOh3te91SNDe2o4RHg=="],
|
||||||
|
|
||||||
|
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.6", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-6OiaAaEbLB6dEkRbQyNzFSJv5HDvly3Mc6q/qcPd2uS/g3szR8wAIkh7UndAFKfMypNSTuZ6eCBmgCLR5LacTg=="],
|
||||||
|
|
||||||
|
"@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-xP5YXbOVRVN8A4pDnSUkEUsL9fYFU6VNhxo8tgr13YnMbf3Pn4xVr+hSyLVjS1Frfi1Uk03ET5Bwml4+0CeYEw=="],
|
||||||
|
|
||||||
|
"@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.6", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-jhH7nJuaOpnTFcuZpWK9dqb6Ge2yGi1okTo0W6wkJrfwAm2vwmO74tF1v07JmrSyHBcKLQATEexclJw9K1Vj7w=="],
|
||||||
|
|
||||||
|
"@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.6", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-olIfZ230B64TvPD6b0tPvrEp2eB0FkyL3KvDlqF4RVmIc/kn3orzXnV6DTQdOOW5UU+M5zKY3/BU47X420/oPw=="],
|
||||||
|
|
||||||
|
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="],
|
||||||
|
|
||||||
|
"@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.7", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-CIbCTGGX5CI7tfewBPSYD9ycp2Vb2GW5xnXD1n7GcO9mu37EN7A6DvCHM9MX7pOeS1adMn5D+1yRwI3eABVbcA=="],
|
||||||
|
|
||||||
|
"@smithy/hash-node": ["@smithy/hash-node@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3Dy9VNR37wfMh2/1RHkFf/e0rMyN0pjY0FdyY6ItJRjENYyVPRMwad6ZR1S9HFm6tTuIOd9pqKBmtJ4VHxvxg=="],
|
||||||
|
|
||||||
|
"@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-+3T8LkH39YIhYHsv/Ec8lF+92nykZpU+XMBvAyXF/uLcTp86pxa5oSJk1vzaRY9N++qgDLYjzJ6OVbtAgDGwfw=="],
|
||||||
|
|
||||||
|
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-E4t/V/q2T46RY21fpfznd1iSLTvCXKNKo4zJ1QuEFN4SE9gKfu2vb6bgq35LpufkQ+SETWIC7ZAf2GGvTlBaMQ=="],
|
||||||
|
|
||||||
|
"@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.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZXeh8UmH31JdcNsrQ1o9v1IVuva9JFwxIc6zTMxWX7wcmWvVR7Ai9aUEw5LraNKqdkAsb06clpM2sRH4Iy55Sg=="],
|
||||||
|
|
||||||
|
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-0cjqjyfj+Gls30ntq45SsBtqF3dfJQCeqQPyGz58Pk8OgrAr5YiB7ZvDzjCA94p4r6DCI4qLm7FKobqBjf515w=="],
|
||||||
|
|
||||||
|
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.0", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-serde": "^4.2.7", "@smithy/node-config-provider": "^4.3.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A=="],
|
||||||
|
|
||||||
|
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.16", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/protocol-http": "^5.3.6", "@smithy/service-error-classification": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-retry": "^4.2.6", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-XPpNhNRzm3vhYm7YCsyw3AtmWggJbg1wNGAoqb7NBYr5XA5isMRv14jgbYyUV6IvbTBFZQdf2QpeW43LrRdStQ=="],
|
||||||
|
|
||||||
|
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="],
|
||||||
|
|
||||||
|
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA=="],
|
||||||
|
|
||||||
|
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.6", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA=="],
|
||||||
|
|
||||||
|
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.6", "", { "dependencies": { "@smithy/abort-controller": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Gsb9jf4ido5BhPfani4ggyrKDd3ZK+vTFWmUaZeFg5G3E5nhFmqiTzAIbHqmPs1sARuJawDiGMGR/nY+Gw6+aQ=="],
|
||||||
|
|
||||||
|
"@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="],
|
||||||
|
|
||||||
|
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="],
|
||||||
|
|
||||||
|
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-MeM9fTAiD3HvoInK/aA8mgJaKQDvm8N0dKy6EiFaCfgpovQr4CaOkJC28XqlSRABM+sHdSQXbC8NZ0DShBMHqg=="],
|
||||||
|
|
||||||
|
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA=="],
|
||||||
|
|
||||||
|
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0" } }, "sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg=="],
|
||||||
|
|
||||||
|
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="],
|
||||||
|
|
||||||
|
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="],
|
||||||
|
|
||||||
|
"@smithy/smithy-client": ["@smithy/smithy-client@4.10.1", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-stack": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" } }, "sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA=="],
|
||||||
|
|
||||||
|
"@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="],
|
||||||
|
|
||||||
|
"@smithy/url-parser": ["@smithy/url-parser@4.2.6", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="],
|
||||||
|
|
||||||
|
"@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="],
|
||||||
|
|
||||||
|
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="],
|
||||||
|
|
||||||
|
"@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.15", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-LiZQVAg/oO8kueX4c+oMls5njaD2cRLXRfcjlTYjhIqmwHnCwkQO5B3dMQH0c5PACILxGAQf6Mxsq7CjlDc76A=="],
|
||||||
|
|
||||||
|
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.18", "", { "dependencies": { "@smithy/config-resolver": "^4.4.4", "@smithy/credential-provider-imds": "^4.2.6", "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Kw2J+KzYm9C9Z9nY6+W0tEnoZOofstVCMTshli9jhQbQCy64rueGfKzPfuFBnVUqZD9JobxTh2DzHmPkp/Va/Q=="],
|
||||||
|
|
||||||
|
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-v60VNM2+mPvgHCBXEfMCYrQ0RepP6u6xvbAkMenfe4Mi872CqNkJzgcnQL837e8NdeDxBgrWQRTluKq5Lqdhfg=="],
|
||||||
|
|
||||||
|
"@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.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="],
|
||||||
|
|
||||||
|
"@smithy/util-retry": ["@smithy/util-retry@4.2.6", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-x7CeDQLPQ9cb6xN7fRJEjlP9NyGW/YeXWc4j/RUhg4I+H60F0PEeRc2c/z3rm9zmsdiMFzpV/rT+4UHW6KM1SA=="],
|
||||||
|
|
||||||
|
"@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.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-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="],
|
||||||
|
|
||||||
|
"@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.6", "", { "dependencies": { "@smithy/abort-controller": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-xU9HwUSik9UUCJmm530yvBy0AwlQFICveKmqvaaTukKkXEAhyiBdHtSrhPrH3rH+uz0ykyaE3LdgsX86C6mDCQ=="],
|
||||||
|
|
||||||
|
"@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||||
|
|
||||||
|
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
|
||||||
|
|
||||||
|
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||||
|
|
||||||
|
"@types/culori": ["@types/culori@4.0.1", "", {}, "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ=="],
|
||||||
|
|
||||||
|
"@types/date-fns": ["@types/date-fns@2.6.3", "", { "dependencies": { "date-fns": "*" } }, "sha512-Ke1lw2Ni1t/wMUoLtKFmSNCLozcTBd6vmMqFP4hRzXn6qzkNt97bPAX0x5Y/c15DP43kKvwW1ycStD5+43jVQA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
|
||||||
|
|
||||||
|
"@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-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@types/uuid": ["@types/uuid@11.0.0", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="],
|
||||||
|
|
||||||
|
"@vibrant/color": ["@vibrant/color@4.0.0", "", {}, "sha512-S9ItdqS1135wTXoIIqAJu8df9dqlOo6Boc5Y4MGsBTu9UmUOvOwfj5b4Ga6S5yrLAKmKYIactkz7zYJdMddkig=="],
|
||||||
|
|
||||||
|
"@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/generator": ["@vibrant/generator@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/types": "^4.0.0" } }, "sha512-CqKAjmgHVDXJVo3Q5+9pUJOvksR7cN3bzx/6MbURYh7lA4rhsIewkUK155M6q0vfcUN3ETi/eTneCi0tLuM2Sg=="],
|
||||||
|
|
||||||
|
"@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/image": ["@vibrant/image@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0" } }, "sha512-Asv/7R/L701norosgvbjOVkodFiwcFihkXixA/gbAd6C+5GCts1Wm1NPk14FNKnM7eKkfAN+0wwPkdOH+PY/YA=="],
|
||||||
|
|
||||||
|
"@vibrant/image-browser": ["@vibrant/image-browser@4.0.0", "", { "dependencies": { "@vibrant/image": "^4.0.0" } }, "sha512-mXckzvJWiP575Y/wNtP87W/TPgyJoGlPBjW4E9YmNS6n4Jb6RqyHQA0ZVulqDslOxjSsihDzY7gpAORRclaoLg=="],
|
||||||
|
|
||||||
|
"@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/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-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/types": ["@vibrant/types@4.0.0", "", {}, "sha512-tA5TAbuROXcPkt+PWjmGfoaiEXyySVaNnCZovf6vXhCbMdrTTCQXvNCde2geiVl6YwtuU/Qrj9iZxS5jZ6yVIw=="],
|
||||||
|
|
||||||
|
"@vibrant/worker": ["@vibrant/worker@4.0.0", "", { "dependencies": { "@vibrant/types": "^4.0.0" } }, "sha512-nSaZZwWQKOgN/nPYUAIRF0/uoa7KpK91A+gjLmZZDgfN1enqxaiihmn+75ayNadW0c6cxAEpEFEHTONR5u9tMw=="],
|
||||||
|
|
||||||
|
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||||
|
|
||||||
|
"any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="],
|
||||||
|
|
||||||
|
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||||
|
|
||||||
|
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
|
||||||
|
|
||||||
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
|
"bmp-js": ["bmp-js@0.1.0", "", {}, "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="],
|
||||||
|
|
||||||
|
"bowser": ["bowser@2.13.1", "", {}, "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw=="],
|
||||||
|
|
||||||
|
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||||
|
|
||||||
|
"c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="],
|
||||||
|
|
||||||
|
"chevrotain": ["chevrotain@10.5.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||||
|
|
||||||
|
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||||
|
|
||||||
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
|
||||||
|
|
||||||
|
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||||
|
|
||||||
|
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"culori": ["culori@4.0.2", "", {}, "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw=="],
|
||||||
|
|
||||||
|
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||||
|
|
||||||
|
"date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
|
||||||
|
|
||||||
|
"deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="],
|
||||||
|
|
||||||
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||||
|
|
||||||
|
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||||
|
|
||||||
|
"effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="],
|
||||||
|
|
||||||
|
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
||||||
|
|
||||||
|
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||||
|
|
||||||
|
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||||
|
|
||||||
|
"exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="],
|
||||||
|
|
||||||
|
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||||
|
|
||||||
|
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
||||||
|
|
||||||
|
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
|
||||||
|
|
||||||
|
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
|
||||||
|
|
||||||
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
||||||
|
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
||||||
|
|
||||||
|
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
|
|
||||||
|
"get-port-please": ["get-port-please@3.1.2", "", {}, "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ=="],
|
||||||
|
|
||||||
|
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
||||||
|
|
||||||
|
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="],
|
||||||
|
|
||||||
|
"http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
|
||||||
|
|
||||||
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
|
"image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="],
|
||||||
|
|
||||||
|
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"isomorphic-fetch": ["isomorphic-fetch@3.0.0", "", { "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" } }, "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
|
||||||
|
|
||||||
|
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
||||||
|
|
||||||
|
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
|
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||||
|
|
||||||
|
"lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@0.561.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="],
|
||||||
|
|
||||||
|
"named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"next": ["next@16.0.10", "", { "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.10", "@next/swc-darwin-x64": "16.0.10", "@next/swc-linux-arm64-gnu": "16.0.10", "@next/swc-linux-arm64-musl": "16.0.10", "@next/swc-linux-x64-gnu": "16.0.10", "@next/swc-linux-x64-musl": "16.0.10", "@next/swc-win32-arm64-msvc": "16.0.10", "@next/swc-win32-x64-msvc": "16.0.10", "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": "dist/bin/next" }, "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
|
||||||
|
|
||||||
|
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||||
|
|
||||||
|
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
||||||
|
|
||||||
|
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
|
||||||
|
|
||||||
|
"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-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
|
||||||
|
|
||||||
|
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
|
||||||
|
|
||||||
|
"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-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
|
||||||
|
|
||||||
|
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||||
|
|
||||||
|
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"pixelmatch": ["pixelmatch@4.0.2", "", { "dependencies": { "pngjs": "^3.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA=="],
|
||||||
|
|
||||||
|
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||||
|
|
||||||
|
"pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
|
||||||
|
|
||||||
|
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
|
||||||
|
|
||||||
|
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||||
|
|
||||||
|
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||||
|
|
||||||
|
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
|
||||||
|
|
||||||
|
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||||
|
|
||||||
|
"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-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-hook-form": ["react-hook-form@7.69.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
|
|
||||||
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
|
"readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
|
||||||
|
|
||||||
|
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
|
||||||
|
|
||||||
|
"regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="],
|
||||||
|
|
||||||
|
"remeda": ["remeda@2.21.3", "", { "dependencies": { "type-fest": "^4.39.1" } }, "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg=="],
|
||||||
|
|
||||||
|
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||||
|
|
||||||
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
|
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
|
||||||
|
|
||||||
|
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
|
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
|
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
||||||
|
|
||||||
|
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
||||||
|
|
||||||
|
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||||
|
|
||||||
|
"strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
|
||||||
|
|
||||||
|
"strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
|
||||||
|
|
||||||
|
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"timm": ["timm@1.7.1", "", {}, "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw=="],
|
||||||
|
|
||||||
|
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
|
||||||
|
|
||||||
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
|
"token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||||
|
|
||||||
|
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||||
|
|
||||||
|
"utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="],
|
||||||
|
|
||||||
|
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
|
||||||
|
|
||||||
|
"valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
|
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
||||||
|
|
||||||
|
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
||||||
|
|
||||||
|
"@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||||
|
|
||||||
|
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||||
|
|
||||||
|
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="],
|
||||||
|
|
||||||
|
"@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="],
|
||||||
|
|
||||||
|
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@6.8.2", "", {}, "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collection/@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=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-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=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-label/@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=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menu/@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=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-popover/@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=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive/@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=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-select/@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=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="],
|
||||||
|
|
||||||
|
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
|
||||||
|
"node-vibrant/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||||
|
|
||||||
|
"pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
|
"pixelmatch/pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="],
|
||||||
|
|
||||||
|
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||||
|
|
||||||
|
"readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||||
|
|
||||||
|
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||||
|
|
||||||
|
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||||
|
|
||||||
|
"node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
|
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
|
|
||||||
|
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
|
|
||||||
|
"@aws-crypto/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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
22
components.json
Normal file
22
components.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
@ -4,4 +4,12 @@ const nextConfig: NextConfig = {
|
|||||||
/* config options here */
|
/* config options here */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
experimental: {
|
||||||
|
serverActions: {
|
||||||
|
bodySizeLimit: '5mb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
37
package.json
37
package.json
@ -10,17 +10,52 @@
|
|||||||
"format": "biome format --write"
|
"format": "biome format --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.954.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
|
"@prisma/client": "^7.2.0",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
"@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-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"culori": "^4.0.2",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"lucide-react": "^0.561.0",
|
||||||
"next": "16.0.10",
|
"next": "16.0.10",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"node-vibrant": "^4.0.3",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"react": "19.2.1",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.1"
|
"react-day-picker": "^9.13.0",
|
||||||
|
"react-dom": "19.2.1",
|
||||||
|
"react-hook-form": "^7.69.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.2.0",
|
"@biomejs/biome": "2.2.0",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/culori": "^4.0.1",
|
||||||
|
"@types/date-fns": "^2.6.3",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/uuid": "^11.0.0",
|
||||||
|
"prisma": "^7.2.0",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
prisma.config.ts
Normal file
14
prisma.config.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// This file was generated by Prisma, and assumes you have installed the following:
|
||||||
|
// npm install --save-dev prisma dotenv
|
||||||
|
import "dotenv/config";
|
||||||
|
import { defineConfig } from "prisma/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "prisma/schema.prisma",
|
||||||
|
migrations: {
|
||||||
|
path: "prisma/migrations",
|
||||||
|
},
|
||||||
|
datasource: {
|
||||||
|
url: process.env["DATABASE_URL"],
|
||||||
|
},
|
||||||
|
});
|
||||||
298
prisma/migrations/20251217222401_artwork_0/migration.sql
Normal file
298
prisma/migrations/20251217222401_artwork_0/migration.sql
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Artwork" (
|
||||||
|
"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,
|
||||||
|
"altText" TEXT,
|
||||||
|
"description" TEXT,
|
||||||
|
"notes" TEXT,
|
||||||
|
"month" INTEGER,
|
||||||
|
"year" INTEGER,
|
||||||
|
"okLabL" DOUBLE PRECISION,
|
||||||
|
"okLabA" DOUBLE PRECISION,
|
||||||
|
"okLabB" DOUBLE PRECISION,
|
||||||
|
"creationDate" TIMESTAMP(3),
|
||||||
|
"needsWork" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"nsfw" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"published" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"setAsHeader" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"fileId" TEXT NOT NULL,
|
||||||
|
"galleryId" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Artwork_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "FileData" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"fileKey" TEXT NOT NULL,
|
||||||
|
"originalFile" TEXT NOT NULL,
|
||||||
|
"fileType" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"fileSize" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "FileData_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Album" (
|
||||||
|
"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,
|
||||||
|
"description" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Album_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Gallery" (
|
||||||
|
"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,
|
||||||
|
"description" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Gallery_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ArtCategory" (
|
||||||
|
"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,
|
||||||
|
"description" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "ArtCategory_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ArtTag" (
|
||||||
|
"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,
|
||||||
|
"description" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "ArtTag_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Color" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"hex" TEXT,
|
||||||
|
"blue" INTEGER,
|
||||||
|
"green" INTEGER,
|
||||||
|
"red" INTEGER,
|
||||||
|
|
||||||
|
CONSTRAINT "Color_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ArtworkColor" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"artworkId" TEXT NOT NULL,
|
||||||
|
"colorId" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ArtworkColor_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ArtworkMetadata" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"artworkId" TEXT NOT NULL,
|
||||||
|
"depth" TEXT NOT NULL,
|
||||||
|
"format" TEXT NOT NULL,
|
||||||
|
"space" TEXT NOT NULL,
|
||||||
|
"channels" INTEGER NOT NULL,
|
||||||
|
"height" INTEGER NOT NULL,
|
||||||
|
"width" INTEGER NOT NULL,
|
||||||
|
"autoOrientH" INTEGER,
|
||||||
|
"autoOrientW" INTEGER,
|
||||||
|
"bitsPerSample" INTEGER,
|
||||||
|
"density" INTEGER,
|
||||||
|
"hasAlpha" BOOLEAN,
|
||||||
|
"hasProfile" BOOLEAN,
|
||||||
|
"isPalette" BOOLEAN,
|
||||||
|
"isProgressive" BOOLEAN,
|
||||||
|
|
||||||
|
CONSTRAINT "ArtworkMetadata_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "FileVariant" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"artworkId" TEXT NOT NULL,
|
||||||
|
"s3Key" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"height" INTEGER NOT NULL,
|
||||||
|
"width" INTEGER NOT NULL,
|
||||||
|
"fileExtension" TEXT,
|
||||||
|
"mimeType" TEXT,
|
||||||
|
"url" TEXT,
|
||||||
|
"sizeBytes" INTEGER,
|
||||||
|
|
||||||
|
CONSTRAINT "FileVariant_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_AlbumToArtwork" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_AlbumToArtwork_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_ArtCategoryToArtwork" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_ArtCategoryToArtwork_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_ArtCategoryToArtTag" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_ArtCategoryToArtTag_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_ArtTagToArtwork" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_ArtTagToArtwork_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Artwork_name_key" ON "Artwork"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Artwork_slug_key" ON "Artwork"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Artwork_fileId_key" ON "Artwork"("fileId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "FileData_fileKey_key" ON "FileData"("fileKey");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "FileData_originalFile_key" ON "FileData"("originalFile");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Album_name_key" ON "Album"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Album_slug_key" ON "Album"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Gallery_name_key" ON "Gallery"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Gallery_slug_key" ON "Gallery"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ArtCategory_name_key" ON "ArtCategory"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ArtCategory_slug_key" ON "ArtCategory"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ArtTag_name_key" ON "ArtTag"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ArtTag_slug_key" ON "ArtTag"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Color_name_key" ON "Color"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ArtworkColor_artworkId_type_key" ON "ArtworkColor"("artworkId", "type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ArtworkMetadata_artworkId_key" ON "ArtworkMetadata"("artworkId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "FileVariant_artworkId_type_key" ON "FileVariant"("artworkId", "type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_AlbumToArtwork_B_index" ON "_AlbumToArtwork"("B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_ArtCategoryToArtwork_B_index" ON "_ArtCategoryToArtwork"("B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_ArtCategoryToArtTag_B_index" ON "_ArtCategoryToArtTag"("B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_ArtTagToArtwork_B_index" ON "_ArtTagToArtwork"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Artwork" ADD CONSTRAINT "Artwork_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileData"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Artwork" ADD CONSTRAINT "Artwork_galleryId_fkey" FOREIGN KEY ("galleryId") REFERENCES "Gallery"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ArtworkColor" ADD CONSTRAINT "ArtworkColor_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ArtworkColor" ADD CONSTRAINT "ArtworkColor_colorId_fkey" FOREIGN KEY ("colorId") REFERENCES "Color"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ArtworkMetadata" ADD CONSTRAINT "ArtworkMetadata_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "FileVariant" ADD CONSTRAINT "FileVariant_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_AlbumToArtwork" ADD CONSTRAINT "_AlbumToArtwork_A_fkey" FOREIGN KEY ("A") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_AlbumToArtwork" ADD CONSTRAINT "_AlbumToArtwork_B_fkey" FOREIGN KEY ("B") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ArtCategoryToArtwork" ADD CONSTRAINT "_ArtCategoryToArtwork_A_fkey" FOREIGN KEY ("A") REFERENCES "ArtCategory"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ArtCategoryToArtwork" ADD CONSTRAINT "_ArtCategoryToArtwork_B_fkey" FOREIGN KEY ("B") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ArtCategoryToArtTag" ADD CONSTRAINT "_ArtCategoryToArtTag_A_fkey" FOREIGN KEY ("A") REFERENCES "ArtCategory"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ArtCategoryToArtTag" ADD CONSTRAINT "_ArtCategoryToArtTag_B_fkey" FOREIGN KEY ("B") REFERENCES "ArtTag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ArtTagToArtwork" ADD CONSTRAINT "_ArtTagToArtwork_A_fkey" FOREIGN KEY ("A") REFERENCES "ArtTag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ArtTagToArtwork" ADD CONSTRAINT "_ArtTagToArtwork_B_fkey" FOREIGN KEY ("B") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
8
prisma/migrations/20251220120018_artwork_1/migration.sql
Normal file
8
prisma/migrations/20251220120018_artwork_1/migration.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `uploadDate` to the `FileVariant` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "FileVariant" ADD COLUMN "uploadDate" TIMESTAMP(3) NOT NULL;
|
||||||
12
prisma/migrations/20251220120200_artwork_2/migration.sql
Normal file
12
prisma/migrations/20251220120200_artwork_2/migration.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `uploadDate` on the `FileVariant` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `uploadDate` to the `FileData` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "FileData" ADD COLUMN "uploadDate" TIMESTAMP(3) NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "FileVariant" DROP COLUMN "uploadDate";
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Artwork" ADD COLUMN "sortKey" INTEGER;
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "postgresql"
|
||||||
203
prisma/schema.prisma
Normal file
203
prisma/schema.prisma
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||||
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client"
|
||||||
|
output = "../src/generated/prisma"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
}
|
||||||
|
|
||||||
|
model Artwork {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String @unique
|
||||||
|
slug String @unique
|
||||||
|
|
||||||
|
altText String?
|
||||||
|
description String?
|
||||||
|
notes String?
|
||||||
|
month Int?
|
||||||
|
year Int?
|
||||||
|
sortKey Int?
|
||||||
|
okLabL Float?
|
||||||
|
okLabA Float?
|
||||||
|
okLabB Float?
|
||||||
|
creationDate DateTime?
|
||||||
|
needsWork Boolean @default(true)
|
||||||
|
nsfw Boolean @default(false)
|
||||||
|
published Boolean @default(false)
|
||||||
|
setAsHeader Boolean @default(false)
|
||||||
|
|
||||||
|
fileId String @unique
|
||||||
|
file FileData @relation(fields: [fileId], references: [id])
|
||||||
|
|
||||||
|
galleryId String?
|
||||||
|
gallery Gallery? @relation(fields: [galleryId], references: [id])
|
||||||
|
|
||||||
|
metadata ArtworkMetadata?
|
||||||
|
|
||||||
|
albums Album[]
|
||||||
|
categories ArtCategory[]
|
||||||
|
colors ArtworkColor[]
|
||||||
|
tags ArtTag[]
|
||||||
|
variants FileVariant[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Album {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String @unique
|
||||||
|
slug String @unique
|
||||||
|
|
||||||
|
description String?
|
||||||
|
|
||||||
|
artworks Artwork[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Gallery {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String @unique
|
||||||
|
slug String @unique
|
||||||
|
|
||||||
|
description String?
|
||||||
|
|
||||||
|
artworks Artwork[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model ArtCategory {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String @unique
|
||||||
|
slug String @unique
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
description String?
|
||||||
|
|
||||||
|
artworks Artwork[]
|
||||||
|
categories ArtCategory[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Color {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
name String @unique
|
||||||
|
type String
|
||||||
|
|
||||||
|
hex String?
|
||||||
|
blue Int?
|
||||||
|
green Int?
|
||||||
|
red Int?
|
||||||
|
|
||||||
|
artworks ArtworkColor[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model ArtworkColor {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
artworkId String
|
||||||
|
colorId String
|
||||||
|
type String
|
||||||
|
|
||||||
|
artwork Artwork @relation(fields: [artworkId], references: [id])
|
||||||
|
color Color @relation(fields: [colorId], references: [id])
|
||||||
|
|
||||||
|
@@unique([artworkId, type])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ArtworkMetadata {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
artworkId String @unique
|
||||||
|
depth String
|
||||||
|
format String
|
||||||
|
space String
|
||||||
|
channels Int
|
||||||
|
height Int
|
||||||
|
width Int
|
||||||
|
|
||||||
|
autoOrientH Int?
|
||||||
|
autoOrientW Int?
|
||||||
|
bitsPerSample Int?
|
||||||
|
density Int?
|
||||||
|
hasAlpha Boolean?
|
||||||
|
hasProfile Boolean?
|
||||||
|
isPalette Boolean?
|
||||||
|
isProgressive Boolean?
|
||||||
|
|
||||||
|
artwork Artwork @relation(fields: [artworkId], references: [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
model FileData {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
fileKey String @unique
|
||||||
|
originalFile String @unique
|
||||||
|
fileType String
|
||||||
|
name String
|
||||||
|
fileSize Int
|
||||||
|
uploadDate DateTime
|
||||||
|
|
||||||
|
artwork Artwork?
|
||||||
|
}
|
||||||
|
|
||||||
|
model FileVariant {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
s3Key String
|
||||||
|
type String
|
||||||
|
height Int
|
||||||
|
width Int
|
||||||
|
|
||||||
|
fileExtension String?
|
||||||
|
mimeType String?
|
||||||
|
url String?
|
||||||
|
sizeBytes Int?
|
||||||
|
|
||||||
|
artworkId String
|
||||||
|
artwork Artwork @relation(fields: [artworkId], references: [id])
|
||||||
|
|
||||||
|
@@unique([artworkId, type])
|
||||||
|
}
|
||||||
67
src/actions/artworks/deleteArtwork.ts
Normal file
67
src/actions/artworks/deleteArtwork.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { s3 } from "@/lib/s3";
|
||||||
|
import { DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
|
||||||
|
export async function deleteArtwork(artworkId: string) {
|
||||||
|
const artwork = await prisma.artwork.findUnique({
|
||||||
|
where: { id: artworkId },
|
||||||
|
include: {
|
||||||
|
variants: true,
|
||||||
|
colors: true,
|
||||||
|
metadata: true,
|
||||||
|
tags: true,
|
||||||
|
categories: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!artwork) throw new Error("Artwork not found");
|
||||||
|
|
||||||
|
// Delete S3 objects
|
||||||
|
for (const variant of artwork.variants) {
|
||||||
|
try {
|
||||||
|
await s3.send(
|
||||||
|
new DeleteObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: variant.s3Key,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to delete S3 object: " + variant.s3Key + ". " + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Delete join entries
|
||||||
|
await prisma.artworkColor.deleteMany({ where: { artworkId } });
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
for (const color of artwork.colors) {
|
||||||
|
const count = await prisma.artworkColor.count({
|
||||||
|
where: { colorId: color.colorId },
|
||||||
|
});
|
||||||
|
if (count === 0) {
|
||||||
|
await prisma.color.delete({ where: { id: color.colorId } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete variants
|
||||||
|
await prisma.fileVariant.deleteMany({ where: { artworkId } });
|
||||||
|
|
||||||
|
// Delete metadata
|
||||||
|
await prisma.artworkMetadata.deleteMany({ where: { artworkId } });
|
||||||
|
|
||||||
|
// Clean many-to-many tag/category joins
|
||||||
|
await prisma.artwork.update({
|
||||||
|
where: { id: artworkId },
|
||||||
|
data: {
|
||||||
|
tags: { set: [] },
|
||||||
|
categories: { set: [] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finally delete the image
|
||||||
|
await prisma.artwork.delete({ where: { id: artworkId } });
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
149
src/actions/artworks/generateArtworkColors.ts
Normal file
149
src/actions/artworks/generateArtworkColors.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { VibrantSwatch } from "@/types/VibrantSwatch";
|
||||||
|
import { getImageBufferFromS3 } 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;
|
||||||
|
|
||||||
|
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) { // start at bit 14
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function centroidFromPaletteHexes(hexByType: Record<string, string | undefined>) {
|
||||||
|
// Tweak weights as you like. Biasing toward Vibrant keeps things “readable”.
|
||||||
|
const weights: Record<string, number> = {
|
||||||
|
Vibrant: 0.7,
|
||||||
|
Muted: 0.15,
|
||||||
|
DarkVibrant: 0.07,
|
||||||
|
DarkMuted: 0.05,
|
||||||
|
LightVibrant: 0.02,
|
||||||
|
LightMuted: 0.01,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure we have at least a vibrant color to anchor on
|
||||||
|
const fallbackHex =
|
||||||
|
hexByType["Vibrant"] ||
|
||||||
|
hexByType["Muted"] ||
|
||||||
|
hexByType["DarkVibrant"] ||
|
||||||
|
hexByType["DarkMuted"] ||
|
||||||
|
hexByType["LightVibrant"] ||
|
||||||
|
hexByType["LightMuted"];
|
||||||
|
|
||||||
|
let L = 0, A = 0, B = 0, W = 0;
|
||||||
|
|
||||||
|
const entries = Object.entries(weights);
|
||||||
|
for (const [type, w] of entries) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (W === 0) {
|
||||||
|
// Should be rare; default to mid-gray
|
||||||
|
return { l: 0.5, a: 0, b: 0 };
|
||||||
|
}
|
||||||
|
return { l: L / W, a: A / W, b: B / W };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateArtworkColors(artworkId: string, fileKey: string, fileType?: string) {
|
||||||
|
const buffer = await getImageBufferFromS3(fileKey, fileType);
|
||||||
|
const palette = await Vibrant.from(buffer).getPalette();
|
||||||
|
|
||||||
|
const vibrantHexes = Object.entries(palette).map(([key, swatch]) => {
|
||||||
|
const castSwatch = swatch as VibrantSwatch | null;
|
||||||
|
const rgb = castSwatch?._rgb;
|
||||||
|
const hex = castSwatch?.hex || (rgb ? rgbToHex(rgb) : undefined);
|
||||||
|
return { type: key, hex };
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { type, hex } of vibrantHexes) {
|
||||||
|
if (!hex) continue;
|
||||||
|
|
||||||
|
const [r, g, b] = hex.match(/\w\w/g)!.map((h) => parseInt(h, 16));
|
||||||
|
const name = generateColorName(hex);
|
||||||
|
|
||||||
|
const color = await prisma.color.upsert({
|
||||||
|
where: { name },
|
||||||
|
create: {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
hex,
|
||||||
|
red: r,
|
||||||
|
green: g,
|
||||||
|
blue: b,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
hex,
|
||||||
|
red: r,
|
||||||
|
green: g,
|
||||||
|
blue: b,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.artworkColor.upsert({
|
||||||
|
where: {
|
||||||
|
artworkId_type: {
|
||||||
|
artworkId,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
artworkId,
|
||||||
|
colorId: color.id,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
colorId: color.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Compute OKLab centroid → Hilbert sortKey (incremental-safe)
|
||||||
|
const hexByType: Record<string, string | undefined> = Object.fromEntries(
|
||||||
|
vibrantHexes.map(({ type, hex }) => [type, hex])
|
||||||
|
);
|
||||||
|
|
||||||
|
const { l, a, b } = centroidFromPaletteHexes(hexByType);
|
||||||
|
const ax = norm(a, A_MIN, A_MAX);
|
||||||
|
const bx = norm(b, B_MIN, B_MAX);
|
||||||
|
const sortKey = hilbertIndex15(ax, bx);
|
||||||
|
|
||||||
|
// 3) Store on the Image (plus optional OKLab fields)
|
||||||
|
await prisma.artwork.update({
|
||||||
|
where: { id: artworkId },
|
||||||
|
data: { sortKey, okLabL: l, okLabA: a, okLabB: b },
|
||||||
|
});
|
||||||
|
|
||||||
|
return await prisma.artworkColor.findMany({
|
||||||
|
where: { artworkId },
|
||||||
|
include: { color: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
81
src/actions/artworks/updateArtwork.ts
Normal file
81
src/actions/artworks/updateArtwork.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { artworkSchema } from "@/schemas/artworks/imageSchema";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export async function updateArtwork(
|
||||||
|
values: z.infer<typeof artworkSchema>,
|
||||||
|
id: string
|
||||||
|
) {
|
||||||
|
const validated = artworkSchema.safeParse(values);
|
||||||
|
// console.log(validated)
|
||||||
|
if (!validated.success) {
|
||||||
|
throw new Error("Invalid image data");
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
needsWork,
|
||||||
|
nsfw,
|
||||||
|
published,
|
||||||
|
setAsHeader,
|
||||||
|
altText,
|
||||||
|
description,
|
||||||
|
notes,
|
||||||
|
month,
|
||||||
|
year,
|
||||||
|
creationDate,
|
||||||
|
tagIds,
|
||||||
|
categoryIds
|
||||||
|
} = validated.data;
|
||||||
|
|
||||||
|
if(setAsHeader) {
|
||||||
|
await prisma.artwork.updateMany({
|
||||||
|
where: { setAsHeader: true },
|
||||||
|
data: { setAsHeader: false },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const updatedArtwork = await prisma.artwork.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
needsWork,
|
||||||
|
nsfw,
|
||||||
|
published,
|
||||||
|
setAsHeader,
|
||||||
|
altText,
|
||||||
|
description,
|
||||||
|
notes,
|
||||||
|
month,
|
||||||
|
year,
|
||||||
|
creationDate
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tagIds) {
|
||||||
|
await prisma.artwork.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: {
|
||||||
|
tags: {
|
||||||
|
set: tagIds.map(id => ({ id }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryIds) {
|
||||||
|
await prisma.artwork.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: {
|
||||||
|
categories: {
|
||||||
|
set: categoryIds.map(id => ({ id }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedArtwork
|
||||||
|
}
|
||||||
41
src/actions/uploads/createBulkImages.ts
Normal file
41
src/actions/uploads/createBulkImages.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { createImageFromFile } from "./createImageFromFile";
|
||||||
|
|
||||||
|
type BulkResult =
|
||||||
|
| { ok: true; artworkId: string; name: string }
|
||||||
|
| { ok: false; name: string; error: string };
|
||||||
|
|
||||||
|
export async function createImagesBulk(formData: FormData): Promise<BulkResult[]> {
|
||||||
|
const entries = formData.getAll("file");
|
||||||
|
const files = entries.filter((x): x is File => x instanceof File);
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error("No files received. Ensure you send FormData with key 'file'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: BulkResult[] = [];
|
||||||
|
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
if (!f.type.startsWith("image/")) {
|
||||||
|
results.push({ ok: false, name: f.name, error: "Unsupported file type" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const artwork = await createImageFromFile(f);
|
||||||
|
if (!artwork) {
|
||||||
|
results.push({ ok: false, name: f.name, error: "Upload failed" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ ok: true, artworkId: artwork.id, name: f.name });
|
||||||
|
} catch (err) {
|
||||||
|
results.push({
|
||||||
|
ok: false,
|
||||||
|
name: f.name,
|
||||||
|
error: err instanceof Error ? err.message : "Upload failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
203
src/actions/uploads/createImage.ts
Normal file
203
src/actions/uploads/createImage.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { fileUploadSchema } from "@/schemas/artworks/imageSchema";
|
||||||
|
import "dotenv/config";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { createImageFromFile } from "./createImageFromFile";
|
||||||
|
|
||||||
|
export async function createImage(values: z.infer<typeof fileUploadSchema>) {
|
||||||
|
const imageFile = values.file[0];
|
||||||
|
return createImageFromFile(imageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export async function createImage(values: z.infer<typeof fileUploadSchema>) {
|
||||||
|
const imageFile = values.file[0];
|
||||||
|
|
||||||
|
if (!(imageFile instanceof File)) {
|
||||||
|
console.log("No image or invalid type");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = imageFile.name;
|
||||||
|
const fileType = imageFile.type;
|
||||||
|
const fileSize = imageFile.size;
|
||||||
|
const lastModified = new Date(imageFile.lastModified);
|
||||||
|
|
||||||
|
const fileKey = uuidv4();
|
||||||
|
|
||||||
|
const arrayBuffer = await imageFile.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
const realFileType = fileType.split("/")[1];
|
||||||
|
const originalKey = `original/${fileKey}.${realFileType}`;
|
||||||
|
const modifiedKey = `modified/${fileKey}.webp`;
|
||||||
|
const resizedKey = `resized/${fileKey}.webp`;
|
||||||
|
const thumbnailKey = `thumbnail/${fileKey}.webp`;
|
||||||
|
|
||||||
|
const sharpData = sharp(buffer);
|
||||||
|
const metadata = await sharpData.metadata();
|
||||||
|
|
||||||
|
//--- Original file
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: originalKey,
|
||||||
|
Body: buffer,
|
||||||
|
ContentType: "image/" + metadata.format,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
//--- Modified file
|
||||||
|
const modifiedBuffer = await sharp(buffer)
|
||||||
|
.toFormat('webp')
|
||||||
|
.toBuffer()
|
||||||
|
const modifiedMetadata = await sharp(modifiedBuffer).metadata();
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: modifiedKey,
|
||||||
|
Body: modifiedBuffer,
|
||||||
|
ContentType: "image/" + modifiedMetadata.format,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
//--- Resized file
|
||||||
|
const { width, height } = modifiedMetadata;
|
||||||
|
const targetSize = 400;
|
||||||
|
let resizeOptions;
|
||||||
|
if (width && height) {
|
||||||
|
if (height < width) {
|
||||||
|
resizeOptions = { height: targetSize };
|
||||||
|
} else {
|
||||||
|
resizeOptions = { width: targetSize };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resizeOptions = { height: targetSize };
|
||||||
|
}
|
||||||
|
const resizedBuffer = await sharp(modifiedBuffer)
|
||||||
|
.resize({ ...resizeOptions, withoutEnlargement: true })
|
||||||
|
.toFormat('webp')
|
||||||
|
.toBuffer();
|
||||||
|
const resizedMetadata = await sharp(resizedBuffer).metadata();
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: resizedKey,
|
||||||
|
Body: resizedBuffer,
|
||||||
|
ContentType: "image/" + resizedMetadata.format,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
//--- Thumbnail file
|
||||||
|
const thumbnailTargetSize = 160;
|
||||||
|
let thumbnailOptions;
|
||||||
|
if (width && height) {
|
||||||
|
if (height < width) {
|
||||||
|
thumbnailOptions = { height: thumbnailTargetSize };
|
||||||
|
} else {
|
||||||
|
thumbnailOptions = { width: thumbnailTargetSize };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
thumbnailOptions = { height: thumbnailTargetSize };
|
||||||
|
}
|
||||||
|
const thumbnailBuffer = await sharp(modifiedBuffer)
|
||||||
|
.resize({ ...thumbnailOptions, withoutEnlargement: true })
|
||||||
|
.toFormat('webp')
|
||||||
|
.toBuffer();
|
||||||
|
const thumbnailMetadata = await sharp(thumbnailBuffer).metadata();
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: thumbnailKey,
|
||||||
|
Body: thumbnailBuffer,
|
||||||
|
ContentType: "image/" + thumbnailMetadata.format,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileRecord = await prisma.fileData.create({
|
||||||
|
data: {
|
||||||
|
name: fileName,
|
||||||
|
fileKey,
|
||||||
|
originalFile: fileName,
|
||||||
|
uploadDate: lastModified,
|
||||||
|
fileType: realFileType,
|
||||||
|
fileSize: fileSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const artworkSlug = fileName.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
const artworkRecord = await prisma.artwork.create({
|
||||||
|
data: {
|
||||||
|
name: fileName,
|
||||||
|
slug: artworkSlug,
|
||||||
|
creationDate: lastModified,
|
||||||
|
fileId: fileRecord.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.artworkMetadata.create({
|
||||||
|
data: {
|
||||||
|
artworkId: artworkRecord.id,
|
||||||
|
format: metadata.format || "unknown",
|
||||||
|
width: metadata.width || 0,
|
||||||
|
height: metadata.height || 0,
|
||||||
|
space: metadata.space || "unknown",
|
||||||
|
channels: metadata.channels || 0,
|
||||||
|
depth: metadata.depth || "unknown",
|
||||||
|
density: metadata.density ?? undefined,
|
||||||
|
bitsPerSample: metadata.bitsPerSample ?? undefined,
|
||||||
|
isProgressive: metadata.isProgressive ?? undefined,
|
||||||
|
isPalette: metadata.isPalette ?? undefined,
|
||||||
|
hasProfile: metadata.hasProfile ?? undefined,
|
||||||
|
hasAlpha: metadata.hasAlpha ?? undefined,
|
||||||
|
autoOrientW: metadata.autoOrient?.width ?? undefined,
|
||||||
|
autoOrientH: metadata.autoOrient?.height ?? undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.fileVariant.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
s3Key: originalKey,
|
||||||
|
type: "original",
|
||||||
|
height: metadata.height,
|
||||||
|
width: metadata.width,
|
||||||
|
fileExtension: metadata.format,
|
||||||
|
mimeType: "image/" + metadata.format,
|
||||||
|
sizeBytes: metadata.size,
|
||||||
|
artworkId: artworkRecord.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s3Key: modifiedKey,
|
||||||
|
type: "modified",
|
||||||
|
height: modifiedMetadata.height,
|
||||||
|
width: modifiedMetadata.width,
|
||||||
|
fileExtension: modifiedMetadata.format,
|
||||||
|
mimeType: "image/" + modifiedMetadata.format,
|
||||||
|
sizeBytes: modifiedMetadata.size,
|
||||||
|
artworkId: artworkRecord.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s3Key: resizedKey,
|
||||||
|
type: "resized",
|
||||||
|
height: resizedMetadata.height,
|
||||||
|
width: resizedMetadata.width,
|
||||||
|
fileExtension: resizedMetadata.format,
|
||||||
|
mimeType: "image/" + resizedMetadata.format,
|
||||||
|
sizeBytes: resizedMetadata.size,
|
||||||
|
artworkId: artworkRecord.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s3Key: thumbnailKey,
|
||||||
|
type: "thumbnail",
|
||||||
|
height: thumbnailMetadata.height,
|
||||||
|
width: thumbnailMetadata.width,
|
||||||
|
fileExtension: thumbnailMetadata.format,
|
||||||
|
mimeType: "image/" + thumbnailMetadata.format,
|
||||||
|
sizeBytes: thumbnailMetadata.size,
|
||||||
|
artworkId: artworkRecord.id
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return artworkRecord
|
||||||
|
}
|
||||||
|
*/
|
||||||
198
src/actions/uploads/createImageFromFile.ts
Normal file
198
src/actions/uploads/createImageFromFile.ts
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { s3 } from "@/lib/s3";
|
||||||
|
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import "dotenv/config";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
export async function createImageFromFile(imageFile: File, opts?: { originalName?: string }) {
|
||||||
|
if (!(imageFile instanceof File)) {
|
||||||
|
console.log("No image or invalid type");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = opts?.originalName ?? imageFile.name;
|
||||||
|
const fileType = imageFile.type;
|
||||||
|
const fileSize = imageFile.size;
|
||||||
|
const lastModified = new Date(imageFile.lastModified);
|
||||||
|
|
||||||
|
const fileKey = uuidv4();
|
||||||
|
|
||||||
|
const arrayBuffer = await imageFile.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
const realFileType = fileType.split("/")[1];
|
||||||
|
const originalKey = `original/${fileKey}.${realFileType}`;
|
||||||
|
const modifiedKey = `modified/${fileKey}.webp`;
|
||||||
|
const resizedKey = `resized/${fileKey}.webp`;
|
||||||
|
const thumbnailKey = `thumbnail/${fileKey}.webp`;
|
||||||
|
|
||||||
|
const sharpData = sharp(buffer);
|
||||||
|
const metadata = await sharpData.metadata();
|
||||||
|
|
||||||
|
//--- Original file
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: originalKey,
|
||||||
|
Body: buffer,
|
||||||
|
ContentType: "image/" + metadata.format,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
//--- Modified file
|
||||||
|
const modifiedBuffer = await sharp(buffer).toFormat("webp").toBuffer();
|
||||||
|
const modifiedMetadata = await sharp(modifiedBuffer).metadata();
|
||||||
|
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: modifiedKey,
|
||||||
|
Body: modifiedBuffer,
|
||||||
|
ContentType: "image/" + modifiedMetadata.format,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
//--- Resized file
|
||||||
|
const { width, height } = modifiedMetadata;
|
||||||
|
const targetSize = 400;
|
||||||
|
|
||||||
|
let resizeOptions: { width?: number; height?: number };
|
||||||
|
if (width && height) {
|
||||||
|
resizeOptions = height < width ? { height: targetSize } : { width: targetSize };
|
||||||
|
} else {
|
||||||
|
resizeOptions = { height: targetSize };
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizedBuffer = await sharp(modifiedBuffer)
|
||||||
|
.resize({ ...resizeOptions, withoutEnlargement: true })
|
||||||
|
.toFormat("webp")
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const resizedMetadata = await sharp(resizedBuffer).metadata();
|
||||||
|
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: resizedKey,
|
||||||
|
Body: resizedBuffer,
|
||||||
|
ContentType: "image/" + resizedMetadata.format,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
//--- Thumbnail file
|
||||||
|
const thumbnailTargetSize = 160;
|
||||||
|
|
||||||
|
let thumbnailOptions: { width?: number; height?: number };
|
||||||
|
if (width && height) {
|
||||||
|
thumbnailOptions = height < width ? { height: thumbnailTargetSize } : { width: thumbnailTargetSize };
|
||||||
|
} else {
|
||||||
|
thumbnailOptions = { height: thumbnailTargetSize };
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnailBuffer = await sharp(modifiedBuffer)
|
||||||
|
.resize({ ...thumbnailOptions, withoutEnlargement: true })
|
||||||
|
.toFormat("webp")
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const thumbnailMetadata = await sharp(thumbnailBuffer).metadata();
|
||||||
|
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: thumbnailKey,
|
||||||
|
Body: thumbnailBuffer,
|
||||||
|
ContentType: "image/" + thumbnailMetadata.format,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileRecord = await prisma.fileData.create({
|
||||||
|
data: {
|
||||||
|
name: fileName,
|
||||||
|
fileKey,
|
||||||
|
originalFile: fileName,
|
||||||
|
uploadDate: lastModified,
|
||||||
|
fileType: realFileType,
|
||||||
|
fileSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const artworkSlug = fileName.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
const artworkRecord = await prisma.artwork.create({
|
||||||
|
data: {
|
||||||
|
name: fileName,
|
||||||
|
slug: artworkSlug,
|
||||||
|
creationDate: lastModified,
|
||||||
|
fileId: fileRecord.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.artworkMetadata.create({
|
||||||
|
data: {
|
||||||
|
artworkId: artworkRecord.id,
|
||||||
|
format: metadata.format || "unknown",
|
||||||
|
width: metadata.width || 0,
|
||||||
|
height: metadata.height || 0,
|
||||||
|
space: metadata.space || "unknown",
|
||||||
|
channels: metadata.channels || 0,
|
||||||
|
depth: metadata.depth || "unknown",
|
||||||
|
density: metadata.density ?? undefined,
|
||||||
|
bitsPerSample: metadata.bitsPerSample ?? undefined,
|
||||||
|
isProgressive: metadata.isProgressive ?? undefined,
|
||||||
|
isPalette: metadata.isPalette ?? undefined,
|
||||||
|
hasProfile: metadata.hasProfile ?? undefined,
|
||||||
|
hasAlpha: metadata.hasAlpha ?? undefined,
|
||||||
|
autoOrientW: metadata.autoOrient?.width ?? undefined,
|
||||||
|
autoOrientH: metadata.autoOrient?.height ?? undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.fileVariant.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
s3Key: originalKey,
|
||||||
|
type: "original",
|
||||||
|
height: metadata.height,
|
||||||
|
width: metadata.width,
|
||||||
|
fileExtension: metadata.format,
|
||||||
|
mimeType: "image/" + metadata.format,
|
||||||
|
sizeBytes: metadata.size,
|
||||||
|
artworkId: artworkRecord.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s3Key: modifiedKey,
|
||||||
|
type: "modified",
|
||||||
|
height: modifiedMetadata.height,
|
||||||
|
width: modifiedMetadata.width,
|
||||||
|
fileExtension: modifiedMetadata.format,
|
||||||
|
mimeType: "image/" + modifiedMetadata.format,
|
||||||
|
sizeBytes: modifiedMetadata.size,
|
||||||
|
artworkId: artworkRecord.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s3Key: resizedKey,
|
||||||
|
type: "resized",
|
||||||
|
height: resizedMetadata.height,
|
||||||
|
width: resizedMetadata.width,
|
||||||
|
fileExtension: resizedMetadata.format,
|
||||||
|
mimeType: "image/" + resizedMetadata.format,
|
||||||
|
sizeBytes: resizedMetadata.size,
|
||||||
|
artworkId: artworkRecord.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s3Key: thumbnailKey,
|
||||||
|
type: "thumbnail",
|
||||||
|
height: thumbnailMetadata.height,
|
||||||
|
width: thumbnailMetadata.width,
|
||||||
|
fileExtension: thumbnailMetadata.format,
|
||||||
|
mimeType: "image/" + thumbnailMetadata.format,
|
||||||
|
sizeBytes: thumbnailMetadata.size,
|
||||||
|
artworkId: artworkRecord.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return artworkRecord;
|
||||||
|
}
|
||||||
21
src/app/api/artworks/page/route.ts
Normal file
21
src/app/api/artworks/page/route.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// app/api/artworks/page/route.ts
|
||||||
|
import { getArtworksPage } from "@/lib/queryArtworks";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
// import { getArtworksPage } from "@/lib/artworks/query";
|
||||||
|
|
||||||
|
export async function GET(req: Request) {
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
|
||||||
|
const published = (searchParams.get("published") ?? "all") as
|
||||||
|
| "all"
|
||||||
|
| "published"
|
||||||
|
| "unpublished"
|
||||||
|
| "needsWork";
|
||||||
|
|
||||||
|
const cursor = searchParams.get("cursor") ?? undefined;
|
||||||
|
const take = Number(searchParams.get("take") ?? "48");
|
||||||
|
|
||||||
|
const data = await getArtworksPage({ published, cursor, take });
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
}
|
||||||
34
src/app/api/image/[...key]/route.ts
Normal file
34
src/app/api/image/[...key]/route.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { s3 } from "@/lib/s3";
|
||||||
|
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import "dotenv/config";
|
||||||
|
|
||||||
|
export async function GET(req: Request, { params }: { params: { key: string[] } }) {
|
||||||
|
const { key } = await params;
|
||||||
|
const s3Key = key.join("/");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: s3Key,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await s3.send(command);
|
||||||
|
|
||||||
|
if (!response.Body) {
|
||||||
|
return new Response("No body", { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.ContentType ?? "application/octet-stream";
|
||||||
|
|
||||||
|
return new Response(response.Body as ReadableStream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Cache-Control": "public, max-age=3600",
|
||||||
|
"Content-Disposition": "inline", // use 'attachment' to force download
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
return new Response("Image not found", { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/app/artworks/[id]/page.tsx
Normal file
53
src/app/artworks/[id]/page.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import ArtworkColors from "@/components/artworks/single/ArtworkColors";
|
||||||
|
import ArtworkVariants from "@/components/artworks/single/ArtworkVariants";
|
||||||
|
import DeleteArtworkButton from "@/components/artworks/single/DeleteArtworkButton";
|
||||||
|
import EditArtworkForm from "@/components/artworks/single/EditArtworkForm";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function ArtworkSinglePage({ params }: { params: { id: string } }) {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
const item = await prisma.artwork.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
// album: true,
|
||||||
|
// type: true,
|
||||||
|
file: true,
|
||||||
|
gallery: true,
|
||||||
|
metadata: true,
|
||||||
|
albums: true,
|
||||||
|
categories: true,
|
||||||
|
colors: { include: { color: true } },
|
||||||
|
// sortContexts: true,
|
||||||
|
tags: true,
|
||||||
|
variants: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const categories = await prisma.artCategory.findMany({ orderBy: { sortIndex: "asc" } });
|
||||||
|
const tags = await prisma.artTag.findMany({ orderBy: { sortIndex: "asc" } });
|
||||||
|
|
||||||
|
if (!item) return <div>Artwork with this id not found</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
{item ? <EditArtworkForm artwork={item} tags={tags} categories={categories} /> : 'Artwork not found...'}
|
||||||
|
<div className="mt-6">
|
||||||
|
{item && <DeleteArtworkButton artworkId={item.id} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{item && <ArtworkVariants variants={item.variants} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
{item && <ArtworkColors colors={item.colors} artworkId={item.id} fileKey={item.file.fileKey} fileType={item.file.fileType || ""} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
src/app/artworks/page.tsx
Normal file
88
src/app/artworks/page.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import ArtworkGallery from "@/components/artworks/ArtworkGallery";
|
||||||
|
import FilterBar from "@/components/artworks/FilterBar";
|
||||||
|
import { getArtworksPage } from "@/lib/queryArtworks";
|
||||||
|
import { PlusCircleIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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 >
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,26 +1,681 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
:root {
|
@custom-variant dark (&:is(.dark *));
|
||||||
--background: #ffffff;
|
|
||||||
--foreground: #171717;
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
--radius-3xl: calc(var(--radius) + 12px);
|
||||||
|
--radius-4xl: calc(var(--radius) + 16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
:root {
|
||||||
:root {
|
--radius: 0.65rem;
|
||||||
--background: #0a0a0a;
|
--background: oklch(1 0 0);
|
||||||
--foreground: #ededed;
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.606 0.25 292.717);
|
||||||
|
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.606 0.25 292.717);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.606 0.25 292.717);
|
||||||
|
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.606 0.25 292.717);
|
||||||
|
--brand: oklch(0.623 0.214 259.815);
|
||||||
|
--highlight: oklch(0.852 0.199 91.936);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.541 0.281 293.009);
|
||||||
|
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.541 0.281 293.009);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.541 0.281 293.009);
|
||||||
|
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.541 0.281 293.009);
|
||||||
|
--brand: oklch(0.707 0.165 254.624);
|
||||||
|
--highlight: oklch(0.852 0.199 91.936);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-zinc {
|
||||||
|
--radius: 0.65rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
.dark-zinc {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-red {
|
||||||
|
--radius: 0.65rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.637 0.237 25.331);
|
||||||
|
--primary-foreground: oklch(0.971 0.013 17.38);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.637 0.237 25.331);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.637 0.237 25.331);
|
||||||
|
--sidebar-primary-foreground: oklch(0.971 0.013 17.38);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.637 0.237 25.331);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-red {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.637 0.237 25.331);
|
||||||
|
--primary-foreground: oklch(0.971 0.013 17.38);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.637 0.237 25.331);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.637 0.237 25.331);
|
||||||
|
--sidebar-primary-foreground: oklch(0.971 0.013 17.38);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.637 0.237 25.331);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-rose {
|
||||||
|
--radius: 0.65rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.645 0.246 16.439);
|
||||||
|
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.645 0.246 16.439);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.645 0.246 16.439);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-rose {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.645 0.246 16.439);
|
||||||
|
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.645 0.246 16.439);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.645 0.246 16.439);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-orange {
|
||||||
|
--radius: 0.65rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.705 0.213 47.604);
|
||||||
|
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.705 0.213 47.604);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.705 0.213 47.604);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.705 0.213 47.604);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-orange {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.646 0.222 41.116);
|
||||||
|
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.646 0.222 41.116);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.646 0.222 41.116);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-green {
|
||||||
|
--radius: 0.65rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.723 0.219 149.579);
|
||||||
|
--primary-foreground: oklch(0.982 0.018 155.826);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.723 0.219 149.579);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.723 0.219 149.579);
|
||||||
|
--sidebar-primary-foreground: oklch(0.982 0.018 155.826);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.723 0.219 149.579);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-green {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.696 0.17 162.48);
|
||||||
|
--primary-foreground: oklch(0.393 0.095 152.535);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.527 0.154 150.069);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.696 0.17 162.48);
|
||||||
|
--sidebar-primary-foreground: oklch(0.393 0.095 152.535);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.527 0.154 150.069);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-blue {
|
||||||
|
--radius: 0.65rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.623 0.214 259.815);
|
||||||
|
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.623 0.214 259.815);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.623 0.214 259.815);
|
||||||
|
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.623 0.214 259.815);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-blue {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.546 0.245 262.881);
|
||||||
|
--primary-foreground: oklch(0.379 0.146 265.522);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.546 0.245 262.881);
|
||||||
|
--sidebar-primary-foreground: oklch(0.379 0.146 265.522);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.488 0.243 264.376);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-yellow {
|
||||||
|
--radius: 0.65rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.795 0.184 86.047);
|
||||||
|
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.795 0.184 86.047);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||||
|
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.795 0.184 86.047);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-yellow {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.795 0.184 86.047);
|
||||||
|
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.554 0.135 66.442);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||||
|
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.554 0.135 66.442);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-violet {
|
||||||
|
--radius: 0.65rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.606 0.25 292.717);
|
||||||
|
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.606 0.25 292.717);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.606 0.25 292.717);
|
||||||
|
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.606 0.25 292.717);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-violet {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.541 0.281 293.009);
|
||||||
|
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.541 0.281 293.009);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.541 0.281 293.009);
|
||||||
|
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.541 0.281 293.009);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
import Footer from "@/components/global/Footer";
|
||||||
|
import Header from "@/components/global/Header";
|
||||||
|
import { ThemeProvider } from "@/components/global/ThemeProvider";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
@ -13,8 +17,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Gaertan Art Admin",
|
||||||
description: "Generated by create next app",
|
description: "Admin page for the gaertan.art website",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -23,11 +27,47 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="dark-violet"
|
||||||
|
themes={[
|
||||||
|
'light-zinc',
|
||||||
|
'light-red',
|
||||||
|
'light-rose',
|
||||||
|
'light-orange',
|
||||||
|
'light-green',
|
||||||
|
'light-blue',
|
||||||
|
'light-yellow',
|
||||||
|
'light-violet',
|
||||||
|
'dark-zinc',
|
||||||
|
'dark-red',
|
||||||
|
'dark-rose',
|
||||||
|
'dark-orange',
|
||||||
|
'dark-green',
|
||||||
|
'dark-blue',
|
||||||
|
'dark-yellow',
|
||||||
|
'dark-violet',
|
||||||
|
]}
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<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}
|
{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>
|
||||||
|
</div>
|
||||||
|
<Toaster />
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,65 +1,7 @@
|
|||||||
import Image from "next/image";
|
export default function HomePage() {
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div>
|
||||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
ADMIN HOME
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={100}
|
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
7
src/app/uploads/bulk/page.tsx
Normal file
7
src/app/uploads/bulk/page.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import UploadBulkImageForm from "@/components/uploads/UploadBulkImageForm";
|
||||||
|
|
||||||
|
export default function UploadsBulkPage() {
|
||||||
|
return (
|
||||||
|
<div><UploadBulkImageForm /></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/app/uploads/single/page.tsx
Normal file
7
src/app/uploads/single/page.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import UploadImageForm from "@/components/uploads/UploadImageForm";
|
||||||
|
|
||||||
|
export default function UploadsSinglePage() {
|
||||||
|
return (
|
||||||
|
<div><UploadImageForm /></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
src/components/artworks/ArtworkGallery.tsx
Normal file
99
src/components/artworks/ArtworkGallery.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
// import { PortfolioImage } from "@/generated/prisma";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ArtworkWithRelations } from "@/types/Artwork";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
|
export default function ArtworkGallery({
|
||||||
|
initialArtworks,
|
||||||
|
initialCursor,
|
||||||
|
}: {
|
||||||
|
initialArtworks: ArtworkWithRelations[];
|
||||||
|
initialCursor: string | null;
|
||||||
|
}) {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const published = searchParams.get("published") ?? "all";
|
||||||
|
|
||||||
|
const [artworks, setArtworks] = useState(initialArtworks);
|
||||||
|
const [cursor, setCursor] = useState<string | null>(initialCursor);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (!cursor || loading) return;
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
qs.set("published", published);
|
||||||
|
qs.set("cursor", cursor);
|
||||||
|
qs.set("take", "48");
|
||||||
|
|
||||||
|
const res = await fetch(`/api/artworks/page?${qs.toString()}`, {
|
||||||
|
method: "GET",
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error("Failed to load more artworks");
|
||||||
|
|
||||||
|
const data: { items: ArtworkWithRelations[]; nextCursor: string | null } =
|
||||||
|
await res.json();
|
||||||
|
|
||||||
|
setArtworks((prev) => [...prev, ...data.items]);
|
||||||
|
setCursor(data.nextCursor);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
className="flex flex-wrap gap-4"
|
||||||
|
>
|
||||||
|
{artworks.map((artwork) => (
|
||||||
|
<div key={artwork.id} style={{ width: 200, height: 200 }}>
|
||||||
|
<Link href={`/artworks/${artwork.id}`}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden transition-all duration-100",
|
||||||
|
"w-full h-full",
|
||||||
|
"hover:border-2 border-transparent"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
'--tw-border-opacity': 1,
|
||||||
|
} as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative w-full h-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={`/api/image/thumbnail/${artwork.file.fileKey}.webp`}
|
||||||
|
alt={artwork.altText ?? artwork.name ?? "Image"}
|
||||||
|
fill
|
||||||
|
className={cn("object-cover"
|
||||||
|
)}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{cursor ? (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button onClick={loadMore} disabled={loading}>
|
||||||
|
{loading ? "Loading..." : "Load more"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
175
src/components/artworks/FilterBar.tsx
Normal file
175
src/components/artworks/FilterBar.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// import { PortfolioAlbum, PortfolioType } from "@/generated/prisma";
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
// import BackfillButton from "./BackfillButton";
|
||||||
|
|
||||||
|
type FilterBarProps = {
|
||||||
|
// types: PortfolioType[];
|
||||||
|
// albums: PortfolioAlbum[];
|
||||||
|
// years: number[];
|
||||||
|
// currentType: string;
|
||||||
|
currentPublished: string;
|
||||||
|
// groupBy: "year" | "album";
|
||||||
|
// groupId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FilterBar({
|
||||||
|
// types,
|
||||||
|
// albums,
|
||||||
|
// years,
|
||||||
|
// currentType,
|
||||||
|
currentPublished,
|
||||||
|
// groupBy,
|
||||||
|
// groupId,
|
||||||
|
}: FilterBarProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const params = new URLSearchParams(searchParams);
|
||||||
|
|
||||||
|
const setFilter = (key: string, value: string) => {
|
||||||
|
if (value !== "all") {
|
||||||
|
params.set(key, value);
|
||||||
|
} else {
|
||||||
|
params.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset groupId when switching groupBy
|
||||||
|
// if (key === "groupBy") {
|
||||||
|
// params.delete("year");
|
||||||
|
// params.delete("album");
|
||||||
|
// }
|
||||||
|
|
||||||
|
router.push(`${pathname}?${params.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortHref = `${pathname}/sort?${params.toString()}`;
|
||||||
|
|
||||||
|
const sortByColor = () => {
|
||||||
|
params.set("sortBy", "color");
|
||||||
|
router.push(`${pathname}?${params.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-6 pb-6">
|
||||||
|
{/* GroupBy Toggle */}
|
||||||
|
{/* <div className="flex gap-2 items-center flex-wrap">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Group by:</span>
|
||||||
|
<FilterButton
|
||||||
|
active={groupBy === "year"}
|
||||||
|
label="Year"
|
||||||
|
onClick={() => setFilter("groupBy", "year")}
|
||||||
|
/>
|
||||||
|
<FilterButton
|
||||||
|
active={groupBy === "album"}
|
||||||
|
label="Album"
|
||||||
|
onClick={() => setFilter("groupBy", "album")}
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
{/* Type Filter */}
|
||||||
|
{/* <div className="flex gap-2 items-center flex-wrap">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Type:</span>
|
||||||
|
<FilterButton
|
||||||
|
active={currentType === "all"}
|
||||||
|
label="All"
|
||||||
|
onClick={() => setFilter("type", "all")}
|
||||||
|
/>
|
||||||
|
{types.map((type) => (
|
||||||
|
<FilterButton
|
||||||
|
key={type.id}
|
||||||
|
active={currentType === type.id}
|
||||||
|
label={type.name}
|
||||||
|
onClick={() => setFilter("type", type.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<FilterButton
|
||||||
|
active={currentType === "none"}
|
||||||
|
label="No Type"
|
||||||
|
onClick={() => setFilter("type", "none")}
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Published Filter */}
|
||||||
|
<div className="flex gap-2 items-center flex-wrap">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Status:</span>
|
||||||
|
<FilterButton
|
||||||
|
active={currentPublished === "all"}
|
||||||
|
label="All"
|
||||||
|
onClick={() => setFilter("published", "all")}
|
||||||
|
/>
|
||||||
|
<FilterButton
|
||||||
|
active={currentPublished === "published"}
|
||||||
|
label="Published"
|
||||||
|
onClick={() => setFilter("published", "published")}
|
||||||
|
/>
|
||||||
|
<FilterButton
|
||||||
|
active={currentPublished === "unpublished"}
|
||||||
|
label="Unpublished"
|
||||||
|
onClick={() => setFilter("published", "unpublished")}
|
||||||
|
/>
|
||||||
|
<FilterButton
|
||||||
|
active={currentPublished === "needsWork"}
|
||||||
|
label="Needs work"
|
||||||
|
onClick={() => setFilter("published", "needsWork")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-6 border-b pb-6">
|
||||||
|
{/* Subnavigation for Year or Album */}
|
||||||
|
{/* <div className="flex gap-2 items-center flex-wrap">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
{groupBy === "year" ? "Year:" : "Album:"}
|
||||||
|
</span>
|
||||||
|
<FilterButton
|
||||||
|
active={groupId === "all"}
|
||||||
|
label="All"
|
||||||
|
onClick={() => setFilter(groupBy, "all")}
|
||||||
|
/>
|
||||||
|
{groupBy === "year" &&
|
||||||
|
years.map((year) => (
|
||||||
|
<FilterButton
|
||||||
|
key={year}
|
||||||
|
active={groupId === String(year)}
|
||||||
|
label={String(year)}
|
||||||
|
onClick={() => setFilter("year", String(year))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{groupBy === "album" &&
|
||||||
|
albums.map((album) => (
|
||||||
|
<FilterButton
|
||||||
|
key={album.id}
|
||||||
|
active={groupId === album.id}
|
||||||
|
label={album.name}
|
||||||
|
onClick={() => setFilter("album", album.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilterButton({
|
||||||
|
active,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
active: boolean;
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`px-3 py-1 rounded text-sm border transition ${active
|
||||||
|
? "bg-primary text-white border-primary"
|
||||||
|
: "bg-muted text-muted-foreground hover:bg-muted/70 border-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/components/artworks/single/ArtworkColors.tsx
Normal file
52
src/components/artworks/single/ArtworkColors.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { generateArtworkColors } from "@/actions/artworks/generateArtworkColors";
|
||||||
|
// import { generateImageColors } from "@/actions/portfolio/images/generateImageColors";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArtworkColor, Color } from "@/generated/prisma/client";
|
||||||
|
// import { Color, ImageColor } from "@/generated/prisma";
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
type ColorWithItems = ArtworkColor & {
|
||||||
|
color: Color
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ArtworkColors({ colors: initialColors, artworkId, fileKey, fileType }: { colors: ColorWithItems[], artworkId: string, fileKey: string, fileType?: string }) {
|
||||||
|
const [colors, setColors] = useState(initialColors);
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const handleGenerate = () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
const newColors = await generateArtworkColors(artworkId, fileKey, fileType);
|
||||||
|
setColors(newColors);
|
||||||
|
toast.success("Colors extracted successfully");
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Failed to extract colors");
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h2 className="font-semibold text-lg">Artwork Colors</h2>
|
||||||
|
<Button size="sm" onClick={handleGenerate} disabled={isPending}>
|
||||||
|
{isPending ? "Extracting..." : "Generate Palette"}
|
||||||
|
</Button>
|
||||||
|
</div >
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{colors.map((item) => (
|
||||||
|
<div
|
||||||
|
key={`${item.artworkId}-${item.type}`}
|
||||||
|
className="w-10 h-10 rounded"
|
||||||
|
style={{ backgroundColor: item.color?.hex ?? "#000000" }}
|
||||||
|
title={`${item.type} – ${item.color?.hex}`}
|
||||||
|
></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/components/artworks/single/ArtworkVariants.tsx
Normal file
23
src/components/artworks/single/ArtworkVariants.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// import { ImageVariant } from "@/generated/prisma";
|
||||||
|
// import { formatFileSize } from "@/utils/formatFileSize";
|
||||||
|
import { FileVariant } from "@/generated/prisma/client";
|
||||||
|
import { formatFileSize } from "@/utils/formatFileSize";
|
||||||
|
import NextImage from "next/image";
|
||||||
|
|
||||||
|
export default function ArtworkVariants({ variants }: { variants: FileVariant[] }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 className="font-semibold text-lg mb-2">Variants</h2>
|
||||||
|
<div>
|
||||||
|
{variants.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>
|
||||||
|
{variant.s3Key && (
|
||||||
|
<NextImage src={`/api/image/${variant.s3Key}`} alt={variant.s3Key} width={variant.width} height={variant.height} className="rounded shadow max-w-md" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/components/artworks/single/DeleteArtworkButton.tsx
Normal file
26
src/components/artworks/single/DeleteArtworkButton.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { deleteArtwork } from "@/actions/artworks/deleteArtwork";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export default function DeleteArtworkButton({ artworkId }: { artworkId: string }) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
if (confirm("Are you sure you want to delete this artwork? This action is irreversible.")) {
|
||||||
|
const result = await deleteArtwork(artworkId);
|
||||||
|
if (result?.success) {
|
||||||
|
router.push("/artworks");
|
||||||
|
} else {
|
||||||
|
alert("Failed to delete artwork.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button variant="destructive" onClick={handleDelete}>
|
||||||
|
Delete Artwork
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
425
src/components/artworks/single/EditArtworkForm.tsx
Normal file
425
src/components/artworks/single/EditArtworkForm.tsx
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { updateArtwork } from "@/actions/artworks/updateArtwork";
|
||||||
|
// import { updateImage } from "@/actions/portfolio/images/updateImage";
|
||||||
|
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 { ArtCategory, ArtTag } from "@/generated/prisma/client";
|
||||||
|
// import { Color, ImageColor, ImageMetadata, ImageVariant, PortfolioAlbum, PortfolioCategory, PortfolioImage, PortfolioSortContext, PortfolioTag, PortfolioType } from "@/generated/prisma";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { artworkSchema } from "@/schemas/artworks/imageSchema";
|
||||||
|
// import { imageSchema } from "@/schemas/portfolio/imageSchema";
|
||||||
|
import { ArtworkWithRelations } 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";
|
||||||
|
|
||||||
|
export default function EditArtworkForm({ artwork, categories, tags }:
|
||||||
|
{
|
||||||
|
artwork: ArtworkWithRelations,
|
||||||
|
categories: ArtCategory[]
|
||||||
|
tags: ArtTag[]
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm<z.infer<typeof artworkSchema>>({
|
||||||
|
resolver: zodResolver(artworkSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: artwork.name,
|
||||||
|
needsWork: artwork.needsWork ?? true,
|
||||||
|
nsfw: artwork.nsfw ?? false,
|
||||||
|
published: artwork.published ?? false,
|
||||||
|
setAsHeader: artwork.setAsHeader ?? false,
|
||||||
|
|
||||||
|
altText: artwork.altText || "",
|
||||||
|
description: artwork.description || "",
|
||||||
|
notes: artwork.notes || "",
|
||||||
|
month: artwork.month || undefined,
|
||||||
|
year: artwork.year || undefined,
|
||||||
|
creationDate: artwork.creationDate ? new Date(artwork.creationDate) : undefined,
|
||||||
|
|
||||||
|
// albumId: image.albumId ?? undefined,
|
||||||
|
// typeId: image.typeId ?? undefined,
|
||||||
|
metadataId: artwork.metadata?.id ?? undefined,
|
||||||
|
categoryIds: artwork.categories?.map(cat => cat.id) ?? [],
|
||||||
|
colorIds: artwork.colors?.map(color => color.id) ?? [],
|
||||||
|
// sortContextIds: image.sortContexts?.map(sortContext => sortContext.id) ?? [],
|
||||||
|
tagIds: artwork.tags?.map(tag => tag.id) ?? [],
|
||||||
|
variantIds: artwork.variants?.map(variant => variant.id) ?? [],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof artworkSchema>) {
|
||||||
|
const updatedArtwork = await updateArtwork(values, artwork.id)
|
||||||
|
if (updatedArtwork) {
|
||||||
|
toast.success("Artwork updated")
|
||||||
|
router.push(`/artworks`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
{/* String */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Image name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="The public display name" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="altText"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Alt Text</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Alt for this image" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea {...field} placeholder="A descriptive text to the image" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* Number */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="month"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Creation Month</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
value={field.value ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(e.target.value === '' ? undefined : +e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="year"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Creation Year</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
value={field.value ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(e.target.value === '' ? undefined : +e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* Date */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="creationDate"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col gap-1">
|
||||||
|
<FormLabel>Creation Date</FormLabel>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"pl-3 text-left font-normal",
|
||||||
|
!field.value && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value ? format(field.value, "PPP") : "Pick a date"}
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={field.value}
|
||||||
|
onSelect={(date) => {
|
||||||
|
field.onChange(date)
|
||||||
|
}}
|
||||||
|
initialFocus
|
||||||
|
fromYear={1990}
|
||||||
|
toYear={2030}
|
||||||
|
captionLayout="dropdown"
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* Select */}
|
||||||
|
{/* <FormField
|
||||||
|
control={form.control}
|
||||||
|
name="albumId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Album</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => field.onChange(value === "" ? undefined : value)}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an album" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{albums.map((album) => (
|
||||||
|
<SelectItem key={album.id} value={album.id}>
|
||||||
|
{album.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
{/* <FormField
|
||||||
|
control={form.control}
|
||||||
|
name="typeId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Art Type</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => field.onChange(value === "" ? undefined : value)}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an art type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{types.map((type) => (
|
||||||
|
<SelectItem key={type.id} value={type.id}>
|
||||||
|
{type.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="tagIds"
|
||||||
|
render={({ field }) => {
|
||||||
|
const selectedOptions = tags
|
||||||
|
.filter(tag => field.value?.includes(tag.id))
|
||||||
|
.map(tag => ({ label: tag.name, value: tag.id }));
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Tags</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<MultipleSelector
|
||||||
|
defaultOptions={tags.map(tag => ({
|
||||||
|
label: tag.name,
|
||||||
|
value: tag.id,
|
||||||
|
}))}
|
||||||
|
placeholder="Select tags"
|
||||||
|
hidePlaceholderWhenSelected
|
||||||
|
selectFirstItem
|
||||||
|
value={selectedOptions}
|
||||||
|
onChange={(options) => {
|
||||||
|
const ids = options.map(option => option.value);
|
||||||
|
field.onChange(ids);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="categoryIds"
|
||||||
|
render={({ field }) => {
|
||||||
|
const selectedOptions = categories
|
||||||
|
.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 => ({
|
||||||
|
label: cat.name,
|
||||||
|
value: cat.id,
|
||||||
|
}))}
|
||||||
|
placeholder="Select categories"
|
||||||
|
hidePlaceholderWhenSelected
|
||||||
|
selectFirstItem
|
||||||
|
value={selectedOptions}
|
||||||
|
onChange={(options) => {
|
||||||
|
const ids = options.map(option => option.value);
|
||||||
|
field.onChange(ids);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 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"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>NSFW</FormLabel>
|
||||||
|
<FormDescription>This image contains sensitive or adult content.</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</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"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Set as header image</FormLabel>
|
||||||
|
<FormDescription>Will be the main banner image. Choose a fitting one.</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* Read only */}
|
||||||
|
{/* <FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fileKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Image Key</FormLabel>
|
||||||
|
<FormControl><Input {...field} disabled /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="originalFile"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Original file</FormLabel>
|
||||||
|
<FormControl><Input {...field} disabled /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/components/global/Footer.tsx
Normal file
5
src/components/global/Footer.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<div>Footer</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/components/global/Header.tsx
Normal file
18
src/components/global/Header.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// import { auth } from "@/auth";
|
||||||
|
// import { SignInOutButton } from "../auth/SignInOutButton";
|
||||||
|
import ModeToggle from "./ModeToggle";
|
||||||
|
import TopNav from "./TopNav";
|
||||||
|
|
||||||
|
export default async function Header() {
|
||||||
|
// const session = await auth()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<TopNav />
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
{/* <SignInOutButton session={session} /> */}
|
||||||
|
<ModeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
src/components/global/ModeToggle.tsx
Normal file
93
src/components/global/ModeToggle.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Moon, Sun } from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
const modes = ["light", "dark"] as const
|
||||||
|
const accents = ["zinc", "red", "rose", "orange", "green", "blue", "yellow", "violet"] as const
|
||||||
|
|
||||||
|
const modeIcons = {
|
||||||
|
light: <Sun className="h-4 w-4" />,
|
||||||
|
dark: <Moon className="h-4 w-4" />,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModeToggle() {
|
||||||
|
const { setTheme, theme } = useTheme()
|
||||||
|
const [mode, setMode] = useState("dark")
|
||||||
|
const [accent, setAccent] = useState("violet")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parts = theme?.split("-")
|
||||||
|
if (parts?.length === 2) {
|
||||||
|
setMode(parts[0])
|
||||||
|
setAccent(parts[1])
|
||||||
|
}
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
function updateTheme(newMode: string, newAccent: string) {
|
||||||
|
const fullTheme = `${newMode}-${newAccent}`
|
||||||
|
setTheme(fullTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
const accentColorMap: Record<string, string> = {
|
||||||
|
zinc: "text-zinc-600",
|
||||||
|
red: "text-red-600",
|
||||||
|
rose: "text-rose-600",
|
||||||
|
orange: "text-orange-600",
|
||||||
|
green: "text-green-600",
|
||||||
|
blue: "text-blue-600",
|
||||||
|
yellow: "text-yellow-600",
|
||||||
|
violet: "text-violet-600",
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<Select
|
||||||
|
value={mode}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setMode(value)
|
||||||
|
updateTheme(value, accent)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-17.5">
|
||||||
|
<SelectValue
|
||||||
|
placeholder="Mode"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{modes.map((m) => (
|
||||||
|
<SelectItem key={m} value={m}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{modeIcons[m]}
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={accent}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setAccent(value)
|
||||||
|
updateTheme(mode, value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-30">
|
||||||
|
<SelectValue placeholder="Accent" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{accents.map((a) => (
|
||||||
|
<SelectItem key={a} value={a} className={cn(accentColorMap[a])}>
|
||||||
|
{a.charAt(0).toUpperCase() + a.slice(1)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
src/components/global/ThemeProvider.tsx
Normal file
11
src/components/global/ThemeProvider.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
||||||
129
src/components/global/TopNav.tsx
Normal file
129
src/components/global/TopNav.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const uploadItems = [
|
||||||
|
{
|
||||||
|
title: "Single Image",
|
||||||
|
href: "/uploads/single",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Multiple Images",
|
||||||
|
href: "/uploads/bulk",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// const portfolioItems = [
|
||||||
|
// {
|
||||||
|
// title: "Images",
|
||||||
|
// href: "/portfolio/images",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Types",
|
||||||
|
// href: "/portfolio/types",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Albums",
|
||||||
|
// href: "/portfolio/albums",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Categories",
|
||||||
|
// href: "/portfolio/categories",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Tags",
|
||||||
|
// href: "/portfolio/tags",
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
|
||||||
|
export default function TopNav() {
|
||||||
|
return (
|
||||||
|
<NavigationMenu viewport={false}>
|
||||||
|
<NavigationMenuList>
|
||||||
|
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||||
|
<Link href="/">Home</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger>Upload</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent>
|
||||||
|
<ul className="grid w-50 gap-4">
|
||||||
|
{uploadItems.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>
|
||||||
|
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||||
|
<Link href="/artworks">Artworks</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
{/* <NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger>Portfolio</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent>
|
||||||
|
<ul className="grid w-75 gap-4">
|
||||||
|
{portfolioItems.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>
|
||||||
|
<ul className="grid w-75 gap-4">
|
||||||
|
<li>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href="/commissions/types">
|
||||||
|
<div className="text-sm leading-none font-medium">Types</div>
|
||||||
|
<p className="text-muted-foreground line-clamp-2 text-sm leading-snug">
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href="/commissions/board">
|
||||||
|
<div className="text-sm leading-none font-medium">Board</div>
|
||||||
|
<p className="text-muted-foreground line-clamp-2 text-sm leading-snug">
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||||
|
<Link href="/items/commissions/tos">ToS</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem> */}
|
||||||
|
</NavigationMenuList>
|
||||||
|
</NavigationMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
62
src/components/ui/button.tsx
Normal file
62
src/components/ui/button.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
220
src/components/ui/calendar.tsx
Normal file
220
src/components/ui/calendar.tsx
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import {
|
||||||
|
DayPicker,
|
||||||
|
getDefaultClassNames,
|
||||||
|
type DayButton,
|
||||||
|
} from "react-day-picker"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
captionLayout = "label",
|
||||||
|
buttonVariant = "ghost",
|
||||||
|
formatters,
|
||||||
|
components,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||||
|
}) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn(
|
||||||
|
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||||
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||||
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
captionLayout={captionLayout}
|
||||||
|
formatters={{
|
||||||
|
formatMonthDropdown: (date) =>
|
||||||
|
date.toLocaleString("default", { month: "short" }),
|
||||||
|
...formatters,
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
root: cn("w-fit", defaultClassNames.root),
|
||||||
|
months: cn(
|
||||||
|
"flex gap-4 flex-col md:flex-row relative",
|
||||||
|
defaultClassNames.months
|
||||||
|
),
|
||||||
|
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||||
|
nav: cn(
|
||||||
|
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||||
|
defaultClassNames.nav
|
||||||
|
),
|
||||||
|
button_previous: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
|
defaultClassNames.button_previous
|
||||||
|
),
|
||||||
|
button_next: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
|
defaultClassNames.button_next
|
||||||
|
),
|
||||||
|
month_caption: cn(
|
||||||
|
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||||
|
defaultClassNames.month_caption
|
||||||
|
),
|
||||||
|
dropdowns: cn(
|
||||||
|
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||||
|
defaultClassNames.dropdowns
|
||||||
|
),
|
||||||
|
dropdown_root: cn(
|
||||||
|
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||||
|
defaultClassNames.dropdown_root
|
||||||
|
),
|
||||||
|
dropdown: cn(
|
||||||
|
"absolute bg-popover inset-0 opacity-0",
|
||||||
|
defaultClassNames.dropdown
|
||||||
|
),
|
||||||
|
caption_label: cn(
|
||||||
|
"select-none font-medium",
|
||||||
|
captionLayout === "label"
|
||||||
|
? "text-sm"
|
||||||
|
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||||
|
defaultClassNames.caption_label
|
||||||
|
),
|
||||||
|
table: "w-full border-collapse",
|
||||||
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
|
weekday: cn(
|
||||||
|
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||||
|
defaultClassNames.weekday
|
||||||
|
),
|
||||||
|
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||||
|
week_number_header: cn(
|
||||||
|
"select-none w-(--cell-size)",
|
||||||
|
defaultClassNames.week_number_header
|
||||||
|
),
|
||||||
|
week_number: cn(
|
||||||
|
"text-[0.8rem] select-none text-muted-foreground",
|
||||||
|
defaultClassNames.week_number
|
||||||
|
),
|
||||||
|
day: cn(
|
||||||
|
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||||
|
props.showWeekNumber
|
||||||
|
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||||
|
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||||
|
defaultClassNames.day
|
||||||
|
),
|
||||||
|
range_start: cn(
|
||||||
|
"rounded-l-md bg-accent",
|
||||||
|
defaultClassNames.range_start
|
||||||
|
),
|
||||||
|
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||||
|
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||||
|
today: cn(
|
||||||
|
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||||
|
defaultClassNames.today
|
||||||
|
),
|
||||||
|
outside: cn(
|
||||||
|
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||||
|
defaultClassNames.outside
|
||||||
|
),
|
||||||
|
disabled: cn(
|
||||||
|
"text-muted-foreground opacity-50",
|
||||||
|
defaultClassNames.disabled
|
||||||
|
),
|
||||||
|
hidden: cn("invisible", defaultClassNames.hidden),
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Root: ({ className, rootRef, ...props }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="calendar"
|
||||||
|
ref={rootRef}
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Chevron: ({ className, orientation, ...props }) => {
|
||||||
|
if (orientation === "left") {
|
||||||
|
return (
|
||||||
|
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation === "right") {
|
||||||
|
return (
|
||||||
|
<ChevronRightIcon
|
||||||
|
className={cn("size-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
DayButton: CalendarDayButton,
|
||||||
|
WeekNumber: ({ children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<td {...props}>
|
||||||
|
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...components,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarDayButton({
|
||||||
|
className,
|
||||||
|
day,
|
||||||
|
modifiers,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayButton>) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLButtonElement>(null)
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (modifiers.focused) ref.current?.focus()
|
||||||
|
}, [modifiers.focused])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
data-day={day.date.toLocaleDateString()}
|
||||||
|
data-selected-single={
|
||||||
|
modifiers.selected &&
|
||||||
|
!modifiers.range_start &&
|
||||||
|
!modifiers.range_end &&
|
||||||
|
!modifiers.range_middle
|
||||||
|
}
|
||||||
|
data-range-start={modifiers.range_start}
|
||||||
|
data-range-end={modifiers.range_end}
|
||||||
|
data-range-middle={modifiers.range_middle}
|
||||||
|
className={cn(
|
||||||
|
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||||
|
defaultClassNames.day,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Calendar, CalendarDayButton }
|
||||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
184
src/components/ui/command.tsx
Normal file
184
src/components/ui/command.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
import { SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn("overflow-hidden p-0", className)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="command-input-wrapper"
|
||||||
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
143
src/components/ui/dialog.tsx
Normal file
143
src/components/ui/dialog.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
249
src/components/ui/dropdown-menu.tsx
Normal file
249
src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-medium data-inset:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"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 z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent,
|
||||||
|
DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger
|
||||||
|
}
|
||||||
|
|
||||||
167
src/components/ui/form.tsx
Normal file
167
src/components/ui/form.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import type * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
useFormState,
|
||||||
|
type ControllerProps,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues,
|
||||||
|
} from "react-hook-form"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
|
||||||
|
const Form = FormProvider
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext)
|
||||||
|
const itemContext = React.useContext(FormItemContext)
|
||||||
|
const { getFieldState } = useFormContext()
|
||||||
|
const formState = useFormState({ name: fieldContext.name })
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState)
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const id = React.useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
data-slot="form-label"
|
||||||
|
data-error={!!error}
|
||||||
|
className={cn("data-[error=true]:text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
data-slot="form-control"
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-description"
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { error, formMessageId } = useFormField()
|
||||||
|
const body = error ? String(error?.message ?? "") : props.children
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-message"
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-destructive text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
}
|
||||||
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
608
src/components/ui/multiselect.tsx
Normal file
608
src/components/ui/multiselect.tsx
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Command as CommandPrimitive, useCommandState } from 'cmdk';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { forwardRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
disable?: boolean;
|
||||||
|
/** fixed option that can't be removed. */
|
||||||
|
fixed?: boolean;
|
||||||
|
/** Group the options by providing key. */
|
||||||
|
[key: string]: string | boolean | undefined;
|
||||||
|
}
|
||||||
|
interface GroupOption {
|
||||||
|
[key: string]: Option[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MultipleSelectorProps {
|
||||||
|
value?: Option[];
|
||||||
|
defaultOptions?: Option[];
|
||||||
|
/** manually controlled options */
|
||||||
|
options?: Option[];
|
||||||
|
placeholder?: string;
|
||||||
|
/** Loading component. */
|
||||||
|
loadingIndicator?: React.ReactNode;
|
||||||
|
/** Empty component. */
|
||||||
|
emptyIndicator?: React.ReactNode;
|
||||||
|
/** Debounce time for async search. Only work with `onSearch`. */
|
||||||
|
delay?: number;
|
||||||
|
/**
|
||||||
|
* Only work with `onSearch` prop. Trigger search when `onFocus`.
|
||||||
|
* For example, when user click on the input, it will trigger the search to get initial options.
|
||||||
|
**/
|
||||||
|
triggerSearchOnFocus?: boolean;
|
||||||
|
/** async search */
|
||||||
|
onSearch?: (value: string) => Promise<Option[]>;
|
||||||
|
/**
|
||||||
|
* sync search. This search will not showing loadingIndicator.
|
||||||
|
* The rest props are the same as async search.
|
||||||
|
* i.e.: creatable, groupBy, delay.
|
||||||
|
**/
|
||||||
|
onSearchSync?: (value: string) => Option[];
|
||||||
|
onChange?: (options: Option[]) => void;
|
||||||
|
/** Limit the maximum number of selected options. */
|
||||||
|
maxSelected?: number;
|
||||||
|
/** When the number of selected options exceeds the limit, the onMaxSelected will be called. */
|
||||||
|
onMaxSelected?: (maxLimit: number) => void;
|
||||||
|
/** Hide the placeholder when there are options selected. */
|
||||||
|
hidePlaceholderWhenSelected?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
/** Group the options base on provided key. */
|
||||||
|
groupBy?: string;
|
||||||
|
className?: string;
|
||||||
|
badgeClassName?: string;
|
||||||
|
/**
|
||||||
|
* First item selected is a default behavior by cmdk. That is why the default is true.
|
||||||
|
* This is a workaround solution by add a dummy item.
|
||||||
|
*
|
||||||
|
* @reference: https://github.com/pacocoursey/cmdk/issues/171
|
||||||
|
*/
|
||||||
|
selectFirstItem?: boolean;
|
||||||
|
/** Allow user to create option when there is no option matched. */
|
||||||
|
creatable?: boolean;
|
||||||
|
/** Props of `Command` */
|
||||||
|
commandProps?: React.ComponentPropsWithoutRef<typeof Command>;
|
||||||
|
/** Props of `CommandInput` */
|
||||||
|
inputProps?: Omit<
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>,
|
||||||
|
'value' | 'placeholder' | 'disabled'
|
||||||
|
>;
|
||||||
|
/** hide the clear all button. */
|
||||||
|
hideClearAllButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultipleSelectorRef {
|
||||||
|
selectedValue: Option[];
|
||||||
|
input: HTMLInputElement;
|
||||||
|
focus: () => void;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDebounce<T>(value: T, delay?: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transToGroupOption(options: Option[], groupBy?: string) {
|
||||||
|
if (options.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!groupBy) {
|
||||||
|
return {
|
||||||
|
'': options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupOption: GroupOption = {};
|
||||||
|
options.forEach((option) => {
|
||||||
|
const key = (option[groupBy] as string) || '';
|
||||||
|
if (!groupOption[key]) {
|
||||||
|
groupOption[key] = [];
|
||||||
|
}
|
||||||
|
groupOption[key].push(option);
|
||||||
|
});
|
||||||
|
return groupOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePickedOption(groupOption: GroupOption, picked: Option[]) {
|
||||||
|
const cloneOption = JSON.parse(JSON.stringify(groupOption)) as GroupOption;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(cloneOption)) {
|
||||||
|
cloneOption[key] = value.filter((val) => !picked.find((p) => p.value === val.value));
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `CommandEmpty` of shadcn/ui will cause the cmdk empty not rendering correctly.
|
||||||
|
* So we create one and copy the `Empty` implementation from `cmdk`.
|
||||||
|
*
|
||||||
|
* @reference: https://github.com/hsuanyi-chou/shadcn-ui-expansions/issues/34#issuecomment-1949561607
|
||||||
|
**/
|
||||||
|
const CommandEmpty = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.ComponentProps<typeof CommandPrimitive.Empty>
|
||||||
|
>(({ className, ...props }, forwardedRef) => {
|
||||||
|
const render = useCommandState((state) => state.filtered.count === 0);
|
||||||
|
|
||||||
|
if (!render) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={forwardedRef}
|
||||||
|
className={cn('py-6 text-center text-sm', className)}
|
||||||
|
cmdk-empty=""
|
||||||
|
role="presentation"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CommandEmpty.displayName = 'CommandEmpty';
|
||||||
|
|
||||||
|
const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
defaultOptions: arrayDefaultOptions = [],
|
||||||
|
options: arrayOptions,
|
||||||
|
delay,
|
||||||
|
onSearch,
|
||||||
|
onSearchSync,
|
||||||
|
loadingIndicator,
|
||||||
|
emptyIndicator,
|
||||||
|
maxSelected = Number.MAX_SAFE_INTEGER,
|
||||||
|
onMaxSelected,
|
||||||
|
hidePlaceholderWhenSelected,
|
||||||
|
disabled,
|
||||||
|
groupBy,
|
||||||
|
className,
|
||||||
|
badgeClassName,
|
||||||
|
selectFirstItem = true,
|
||||||
|
creatable = false,
|
||||||
|
triggerSearchOnFocus = false,
|
||||||
|
commandProps,
|
||||||
|
inputProps,
|
||||||
|
hideClearAllButton = false,
|
||||||
|
}: MultipleSelectorProps,
|
||||||
|
ref: React.Ref<MultipleSelectorRef>,
|
||||||
|
) => {
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const [onScrollbar, setOnScrollbar] = React.useState(false);
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
const dropdownRef = React.useRef<HTMLDivElement>(null); // Added this
|
||||||
|
|
||||||
|
const [selected, setSelected] = React.useState<Option[]>(value || []);
|
||||||
|
const [options, setOptions] = React.useState<GroupOption>(
|
||||||
|
transToGroupOption(arrayDefaultOptions, groupBy),
|
||||||
|
);
|
||||||
|
const [inputValue, setInputValue] = React.useState('');
|
||||||
|
const debouncedSearchTerm = useDebounce(inputValue, delay || 500);
|
||||||
|
|
||||||
|
React.useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
selectedValue: [...selected],
|
||||||
|
input: inputRef.current as HTMLInputElement,
|
||||||
|
focus: () => inputRef?.current?.focus(),
|
||||||
|
reset: () => setSelected([]),
|
||||||
|
}),
|
||||||
|
[selected],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node) &&
|
||||||
|
inputRef.current &&
|
||||||
|
!inputRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setOpen(false);
|
||||||
|
inputRef.current.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnselect = React.useCallback(
|
||||||
|
(option: Option) => {
|
||||||
|
const newOptions = selected.filter((s) => s.value !== option.value);
|
||||||
|
setSelected(newOptions);
|
||||||
|
onChange?.(newOptions);
|
||||||
|
},
|
||||||
|
[onChange, selected],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
const input = inputRef.current;
|
||||||
|
if (input) {
|
||||||
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
|
if (input.value === '' && selected.length > 0) {
|
||||||
|
const lastSelectOption = selected[selected.length - 1];
|
||||||
|
// If there is a last item and it is not fixed, we can remove it.
|
||||||
|
if (lastSelectOption && !lastSelectOption.fixed) {
|
||||||
|
handleUnselect(lastSelectOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This is not a default behavior of the <input /> field
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
input.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleUnselect, selected],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('touchend', handleClickOutside);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('touchend', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('touchend', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) {
|
||||||
|
setSelected(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/** If `onSearch` is provided, do not trigger options updated. */
|
||||||
|
if (!arrayOptions || onSearch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newOption = transToGroupOption(arrayOptions || [], groupBy);
|
||||||
|
if (JSON.stringify(newOption) !== JSON.stringify(options)) {
|
||||||
|
setOptions(newOption);
|
||||||
|
}
|
||||||
|
}, [arrayDefaultOptions, arrayOptions, groupBy, onSearch, options]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/** sync search */
|
||||||
|
|
||||||
|
const doSearchSync = () => {
|
||||||
|
const res = onSearchSync?.(debouncedSearchTerm);
|
||||||
|
setOptions(transToGroupOption(res || [], groupBy));
|
||||||
|
};
|
||||||
|
|
||||||
|
const exec = async () => {
|
||||||
|
if (!onSearchSync || !open) return;
|
||||||
|
|
||||||
|
if (triggerSearchOnFocus) {
|
||||||
|
doSearchSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debouncedSearchTerm) {
|
||||||
|
doSearchSync();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void exec();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/** async search */
|
||||||
|
|
||||||
|
const doSearch = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const res = await onSearch?.(debouncedSearchTerm);
|
||||||
|
setOptions(transToGroupOption(res || [], groupBy));
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exec = async () => {
|
||||||
|
if (!onSearch || !open) return;
|
||||||
|
|
||||||
|
if (triggerSearchOnFocus) {
|
||||||
|
await doSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debouncedSearchTerm) {
|
||||||
|
await doSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void exec();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]);
|
||||||
|
|
||||||
|
const CreatableItem = () => {
|
||||||
|
if (!creatable) return undefined;
|
||||||
|
if (
|
||||||
|
isOptionsExist(options, [{ value: inputValue, label: inputValue }]) ||
|
||||||
|
selected.find((s) => s.value === inputValue)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item = (
|
||||||
|
<CommandItem
|
||||||
|
value={inputValue}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onSelect={(value: string) => {
|
||||||
|
if (selected.length >= maxSelected) {
|
||||||
|
onMaxSelected?.(selected.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputValue('');
|
||||||
|
const newOptions = [...selected, { value, label: value }];
|
||||||
|
setSelected(newOptions);
|
||||||
|
onChange?.(newOptions);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`Create "${inputValue}"`}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
// For normal creatable
|
||||||
|
if (!onSearch && inputValue.length > 0) {
|
||||||
|
return Item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For async search creatable. avoid showing creatable item before loading at first.
|
||||||
|
if (onSearch && debouncedSearchTerm.length > 0 && !isLoading) {
|
||||||
|
return Item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmptyItem = React.useCallback(() => {
|
||||||
|
if (!emptyIndicator) return undefined;
|
||||||
|
|
||||||
|
// For async search that showing emptyIndicator
|
||||||
|
if (onSearch && !creatable && Object.keys(options).length === 0) {
|
||||||
|
return (
|
||||||
|
<CommandItem value="-" disabled>
|
||||||
|
{emptyIndicator}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <CommandEmpty>{emptyIndicator}</CommandEmpty>;
|
||||||
|
}, [creatable, emptyIndicator, onSearch, options]);
|
||||||
|
|
||||||
|
const selectables = React.useMemo<GroupOption>(
|
||||||
|
() => removePickedOption(options, selected),
|
||||||
|
[options, selected],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Avoid Creatable Selector freezing or lagging when paste a long string. */
|
||||||
|
const commandFilter = React.useCallback(() => {
|
||||||
|
if (commandProps?.filter) {
|
||||||
|
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 (
|
||||||
|
<Command
|
||||||
|
ref={dropdownRef}
|
||||||
|
{...commandProps}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
handleKeyDown(e);
|
||||||
|
commandProps?.onKeyDown?.(e);
|
||||||
|
}}
|
||||||
|
className={cn('h-auto overflow-visible bg-transparent', commandProps?.className)}
|
||||||
|
shouldFilter={
|
||||||
|
commandProps?.shouldFilter !== undefined ? commandProps.shouldFilter : !onSearch
|
||||||
|
} // When onSearch is provided, we don't want to filter the options. You can still override it.
|
||||||
|
filter={commandFilter()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'min-h-10 rounded-md border border-input text-base ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 md:text-sm',
|
||||||
|
{
|
||||||
|
'px-3 py-2': selected.length !== 0,
|
||||||
|
'cursor-text': !disabled && selected.length !== 0,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (disabled) return;
|
||||||
|
inputRef?.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="relative flex flex-wrap gap-1">
|
||||||
|
{selected.map((option) => {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={option.value}
|
||||||
|
className={cn(
|
||||||
|
'data-[disabled]:bg-muted-foreground data-[disabled]:text-muted data-[disabled]:hover:bg-muted-foreground',
|
||||||
|
'data-[fixed]:bg-muted-foreground data-[fixed]:text-muted data-[fixed]:hover:bg-muted-foreground',
|
||||||
|
badgeClassName,
|
||||||
|
)}
|
||||||
|
data-fixed={option.fixed}
|
||||||
|
data-disabled={disabled || undefined}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
'ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||||
|
(disabled || option.fixed) && 'hidden',
|
||||||
|
)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleUnselect(option);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={() => handleUnselect(option)}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* Avoid having the "Search" Icon */}
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
{...inputProps}
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
disabled={disabled}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setInputValue(value);
|
||||||
|
inputProps?.onValueChange?.(value);
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
if (!onScrollbar) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
inputProps?.onBlur?.(event);
|
||||||
|
}}
|
||||||
|
onFocus={(event) => {
|
||||||
|
setOpen(true);
|
||||||
|
inputProps?.onFocus?.(event);
|
||||||
|
}}
|
||||||
|
placeholder={hidePlaceholderWhenSelected && selected.length !== 0 ? '' : placeholder}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 bg-transparent outline-none placeholder:text-muted-foreground',
|
||||||
|
{
|
||||||
|
'w-full': hidePlaceholderWhenSelected,
|
||||||
|
'px-3 py-2': selected.length === 0,
|
||||||
|
'ml-1': selected.length !== 0,
|
||||||
|
},
|
||||||
|
inputProps?.className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(selected.filter((s) => s.fixed));
|
||||||
|
onChange?.(selected.filter((s) => s.fixed));
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'absolute ltr:right-0 rtl:left-0 h-6 w-6 p-0',
|
||||||
|
(hideClearAllButton ||
|
||||||
|
disabled ||
|
||||||
|
selected.length < 1 ||
|
||||||
|
selected.filter((s) => s.fixed).length === selected.length) &&
|
||||||
|
'hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
{open && (
|
||||||
|
<CommandList
|
||||||
|
className="absolute top-1 z-10 w-full rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in"
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setOnScrollbar(false);
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setOnScrollbar(true);
|
||||||
|
}}
|
||||||
|
onMouseUp={() => {
|
||||||
|
inputRef?.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>{loadingIndicator}</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{EmptyItem()}
|
||||||
|
{CreatableItem()}
|
||||||
|
{!selectFirstItem && <CommandItem value="-" className="hidden" />}
|
||||||
|
{Object.entries(selectables).map(([key, dropdowns]) => (
|
||||||
|
<CommandGroup key={key} heading={key} className="h-full overflow-auto">
|
||||||
|
<>
|
||||||
|
{dropdowns.map((option) => {
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.label}
|
||||||
|
disabled={option.disable}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onSelect={() => {
|
||||||
|
if (selected.length >= maxSelected) {
|
||||||
|
onMaxSelected?.(selected.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputValue('');
|
||||||
|
const newOptions = [...selected, option];
|
||||||
|
setSelected(newOptions);
|
||||||
|
onChange?.(newOptions);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer',
|
||||||
|
option.disable && 'cursor-default text-muted-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</CommandGroup>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Command>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
MultipleSelector.displayName = 'MultipleSelector';
|
||||||
|
export default MultipleSelector;
|
||||||
161
src/components/ui/navigation-menu.tsx
Normal file
161
src/components/ui/navigation-menu.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function NavigationMenu({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
viewport = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||||
|
viewport?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
data-slot="navigation-menu"
|
||||||
|
data-viewport={viewport}
|
||||||
|
className={cn(
|
||||||
|
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{viewport && <NavigationMenuViewport />}
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
data-slot="navigation-menu-list"
|
||||||
|
className={cn(
|
||||||
|
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Item
|
||||||
|
data-slot="navigation-menu-item"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
)
|
||||||
|
|
||||||
|
function NavigationMenuTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
data-slot="navigation-menu-trigger"
|
||||||
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="relative top-1px ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
data-slot="navigation-menu-content"
|
||||||
|
className={cn(
|
||||||
|
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||||
|
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuViewport({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
data-slot="navigation-menu-viewport"
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border shadow md:w-(--radix-navigation-menu-viewport-width)",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuLink({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Link
|
||||||
|
data-slot="navigation-menu-link"
|
||||||
|
className={cn(
|
||||||
|
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuIndicator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
data-slot="navigation-menu-indicator"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle, NavigationMenuViewport
|
||||||
|
}
|
||||||
|
|
||||||
48
src/components/ui/popover.tsx
Normal file
48
src/components/ui/popover.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||||
191
src/components/ui/select.tsx
Normal file
191
src/components/ui/select.tsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
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
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "item-aligned",
|
||||||
|
align = "center",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"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
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
align={align}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
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
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-slot="select-item-indicator"
|
||||||
|
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||||
|
>
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
}
|
||||||
|
|
||||||
139
src/components/ui/sheet.tsx
Normal file
139
src/components/ui/sheet.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
data-slot="sheet-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
side = "right",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
data-slot="sheet-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
|
side === "right" &&
|
||||||
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||||
|
side === "left" &&
|
||||||
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||||
|
side === "top" &&
|
||||||
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||||
|
side === "bottom" &&
|
||||||
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
data-slot="sheet-title"
|
||||||
|
className={cn("text-foreground font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
data-slot="sheet-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
}
|
||||||
40
src/components/ui/sonner.tsx
Normal file
40
src/components/ui/sonner.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
CircleCheckIcon,
|
||||||
|
InfoIcon,
|
||||||
|
Loader2Icon,
|
||||||
|
OctagonXIcon,
|
||||||
|
TriangleAlertIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
icons={{
|
||||||
|
success: <CircleCheckIcon className="size-4" />,
|
||||||
|
info: <InfoIcon className="size-4" />,
|
||||||
|
warning: <TriangleAlertIcon className="size-4" />,
|
||||||
|
error: <OctagonXIcon className="size-4" />,
|
||||||
|
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||||
|
}}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
"--border-radius": "var(--radius)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
31
src/components/ui/switch.tsx
Normal file
31
src/components/ui/switch.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
className={cn(
|
||||||
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className={cn(
|
||||||
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
269
src/components/uploads/UploadBulkImageForm.tsx
Normal file
269
src/components/uploads/UploadBulkImageForm.tsx
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createImagesBulk } from "@/actions/uploads/createBulkImages";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { fileUploadSchema } from "@/schemas/artworks/imageSchema"; // keep your existing schema
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
type UploadStatus = "queued" | "uploading" | "done" | "error";
|
||||||
|
|
||||||
|
type UploadItem = {
|
||||||
|
id: string; // clientId
|
||||||
|
file: File;
|
||||||
|
previewUrl: string;
|
||||||
|
status: UploadStatus;
|
||||||
|
progress: number; // 0..100 (simulated)
|
||||||
|
artworkId?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BulkResult =
|
||||||
|
| { ok: true; artworkId: string; name: string }
|
||||||
|
| { ok: false; name: string; error: string };
|
||||||
|
|
||||||
|
function extFromName(name: string) {
|
||||||
|
const idx = name.lastIndexOf(".");
|
||||||
|
if (idx === -1) return "";
|
||||||
|
return name.slice(idx).toLowerCase(); // includes dot
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripExt(name: string) {
|
||||||
|
const idx = name.lastIndexOf(".");
|
||||||
|
if (idx === -1) return name;
|
||||||
|
return name.slice(0, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UploadImagesBulkForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof fileUploadSchema>>({
|
||||||
|
resolver: zodResolver(fileUploadSchema),
|
||||||
|
defaultValues: { file: undefined },
|
||||||
|
});
|
||||||
|
|
||||||
|
const [items, setItems] = useState<UploadItem[]>([]);
|
||||||
|
const intervalsRef = useRef<Map<string, number>>(new Map());
|
||||||
|
|
||||||
|
const isBusy = useMemo(() => items.some((i) => i.status === "uploading"), [items]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// cleanup object urls
|
||||||
|
for (const it of items) URL.revokeObjectURL(it.previewUrl);
|
||||||
|
// cleanup intervals
|
||||||
|
for (const id of intervalsRef.current.values()) window.clearInterval(id);
|
||||||
|
intervalsRef.current.clear();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startSimProgress = (id: string) => {
|
||||||
|
// climb slowly to 90% while server action runs; snap to 100% at end
|
||||||
|
const intervalId = window.setInterval(() => {
|
||||||
|
setItems((prev) =>
|
||||||
|
prev.map((it) => {
|
||||||
|
if (it.id !== id) return it;
|
||||||
|
if (it.status !== "uploading") return it;
|
||||||
|
const next = Math.min(90, it.progress + Math.random() * 7 + 1);
|
||||||
|
return { ...it, progress: next };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
intervalsRef.current.set(id, intervalId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopSimProgress = (id: string) => {
|
||||||
|
const intervalId = intervalsRef.current.get(id);
|
||||||
|
if (intervalId) window.clearInterval(intervalId);
|
||||||
|
intervalsRef.current.delete(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAll = () => {
|
||||||
|
setItems((prev) => {
|
||||||
|
for (const it of prev) URL.revokeObjectURL(it.previewUrl);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
for (const intervalId of intervalsRef.current.values()) window.clearInterval(intervalId);
|
||||||
|
intervalsRef.current.clear();
|
||||||
|
form.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
const next: UploadItem[] = Array.from(files).map((file) => ({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
file,
|
||||||
|
previewUrl: URL.createObjectURL(file),
|
||||||
|
status: "queued",
|
||||||
|
progress: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Replace queue (you can change this to append if you prefer)
|
||||||
|
setItems((prev) => {
|
||||||
|
for (const it of prev) URL.revokeObjectURL(it.previewUrl);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
form.setValue("file", files, { shouldValidate: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof fileUploadSchema>) {
|
||||||
|
const files = Array.from(values.file ?? []);
|
||||||
|
if (files.length === 0 || items.length === 0) return;
|
||||||
|
|
||||||
|
// Build FormData with a stable clientId that survives transport by embedding it into the filename.
|
||||||
|
// This lets us map server results back to each preview item reliably, even for duplicate filenames.
|
||||||
|
const fd = new FormData();
|
||||||
|
const clientIdToOriginalName = new Map<string, string>();
|
||||||
|
|
||||||
|
for (const it of items) {
|
||||||
|
const encodedOriginal = encodeURIComponent(it.file.name);
|
||||||
|
fd.append("file", it.file, `${it.id}__${encodedOriginal}`);
|
||||||
|
clientIdToOriginalName.set(it.id, it.file.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI: mark uploading and start simulated progress for each
|
||||||
|
setItems((prev) => prev.map((it) => ({ ...it, status: "uploading", progress: Math.max(it.progress, 5) })));
|
||||||
|
for (const it of items) startSimProgress(it.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = (await createImagesBulk(fd)) as BulkResult[];
|
||||||
|
|
||||||
|
// stop progress intervals
|
||||||
|
for (const it of items) stopSimProgress(it.id);
|
||||||
|
|
||||||
|
// Results matching strategy (least brittle):
|
||||||
|
// Our bulk action returns { name } = f.name which is now "<clientId>__<encodedOriginalName>".
|
||||||
|
// Parse clientId from result.name, map back to item by id.
|
||||||
|
const byClientId = new Map<string, BulkResult>();
|
||||||
|
for (const r of results) {
|
||||||
|
const [clientId] = r.name.split("__", 1);
|
||||||
|
if (clientId) byClientId.set(clientId, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
setItems((prev) =>
|
||||||
|
prev.map((it) => {
|
||||||
|
const r = byClientId.get(it.id);
|
||||||
|
if (!r) {
|
||||||
|
return { ...it, status: "error", progress: 100, error: "No result returned" };
|
||||||
|
}
|
||||||
|
if (r.ok) {
|
||||||
|
return { ...it, status: "done", progress: 100, artworkId: r.artworkId };
|
||||||
|
}
|
||||||
|
return { ...it, status: "error", progress: 100, error: r.error };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const okCount = results.filter((r) => r.ok).length;
|
||||||
|
const failCount = results.length - okCount;
|
||||||
|
|
||||||
|
if (okCount > 0) toast.success(`Uploaded ${okCount} image(s)`);
|
||||||
|
if (failCount > 0) toast.error(`${failCount} image(s) failed`);
|
||||||
|
} catch (err) {
|
||||||
|
for (const it of items) stopSimProgress(it.id);
|
||||||
|
|
||||||
|
setItems((prev) =>
|
||||||
|
prev.map((it) =>
|
||||||
|
it.status === "uploading" ? { ...it, status: "error", progress: 100, error: "Upload failed" } : it
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.error(err instanceof Error ? err.message : "Upload failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="file"
|
||||||
|
render={() => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Choose images to upload</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="file" accept="image/*" multiple onChange={onFileChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button type="submit" disabled={items.length === 0 || isBusy}>
|
||||||
|
{isBusy ? "Uploading..." : "Submit"}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="secondary" disabled={isBusy} onClick={() => router.back()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="outline" disabled={isBusy} onClick={resetAll}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{items.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{items.map((it) => (
|
||||||
|
<div key={it.id} className="rounded-lg border p-3">
|
||||||
|
<div className="relative aspect-square w-full overflow-hidden rounded-md">
|
||||||
|
<Image src={it.previewUrl} alt={it.file.name} fill className="object-cover" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-start justify-between gap-2">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-sm font-medium">{it.file.name}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{it.status === "queued" && "Queued"}
|
||||||
|
{it.status === "uploading" && "Uploading / Processing"}
|
||||||
|
{it.status === "done" && "Done"}
|
||||||
|
{it.status === "error" && "Error"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{it.status === "done" && it.artworkId ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => router.push(`/portfolio/images/${it.artworkId}`)}
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="h-2 w-full rounded bg-muted">
|
||||||
|
<div className="h-2 rounded bg-primary transition-all" style={{ width: `${it.progress}%` }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{it.status === "error" && it.error ? (
|
||||||
|
<div className="mt-2 text-xs text-destructive">{it.error}</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{it.status === "done" && it.artworkId ? (
|
||||||
|
<div className="mt-2 text-xs text-muted-foreground">
|
||||||
|
Artwork ID: <span className="font-mono">{it.artworkId}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
92
src/components/uploads/UploadImageForm.tsx
Normal file
92
src/components/uploads/UploadImageForm.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createImage } from "@/actions/uploads/createImage";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { fileUploadSchema } from "@/schemas/artworks/imageSchema";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
export default function UploadImageForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [preview, setPreview] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof fileUploadSchema>>({
|
||||||
|
resolver: zodResolver(fileUploadSchema),
|
||||||
|
defaultValues: {
|
||||||
|
file: undefined
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
const file = files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onloadend = () => {
|
||||||
|
if (typeof reader.result === 'string') {
|
||||||
|
setPreview(reader.result as string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
form.setValue("file", files);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof fileUploadSchema>) {
|
||||||
|
const image = await createImage(values)
|
||||||
|
if (image) {
|
||||||
|
toast.success("Image created")
|
||||||
|
router.push(`/portfolio/images/${image.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="file"
|
||||||
|
render={() => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Choose image to upload</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={(e) => onFileChange(e)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<div className="flex justify-center p-4">
|
||||||
|
{
|
||||||
|
preview ?
|
||||||
|
<Image
|
||||||
|
src={preview}
|
||||||
|
alt="Preview"
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/lib/prisma.ts
Normal file
10
src/lib/prisma.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
|
import "dotenv/config";
|
||||||
|
import { PrismaClient } from '../generated/prisma/client';
|
||||||
|
|
||||||
|
const connectionString = `${process.env.DATABASE_URL}`
|
||||||
|
|
||||||
|
const adapter = new PrismaPg({ connectionString })
|
||||||
|
const prisma = new PrismaClient({ adapter })
|
||||||
|
|
||||||
|
export { prisma };
|
||||||
43
src/lib/queryArtworks.ts
Normal file
43
src/lib/queryArtworks.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// src/lib/artworks/query.ts
|
||||||
|
import { Prisma } from "@/generated/prisma/client";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export type ArtworkListParams = {
|
||||||
|
published?: string;
|
||||||
|
take?: number;
|
||||||
|
cursor?: string; // artwork.id of the last item from previous page
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getArtworksPage(params: ArtworkListParams) {
|
||||||
|
const { published = "all", take = 48, cursor } = params;
|
||||||
|
|
||||||
|
const where: Prisma.ArtworkWhereInput = {};
|
||||||
|
|
||||||
|
if (published === "published") where.published = true;
|
||||||
|
else if (published === "unpublished") where.published = false;
|
||||||
|
else if (published === "needsWork") where.needsWork = true;
|
||||||
|
|
||||||
|
const artworks = await prisma.artwork.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
file: true,
|
||||||
|
variants: true,
|
||||||
|
metadata: true,
|
||||||
|
},
|
||||||
|
orderBy: [{ createdAt: "desc" }, { id: "asc" }],
|
||||||
|
take: take + 1, // fetch one extra to know if there is a next page
|
||||||
|
...(cursor
|
||||||
|
? {
|
||||||
|
cursor: { id: cursor },
|
||||||
|
skip: 1, // skip the cursor item itself
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasMore = artworks.length > take;
|
||||||
|
const pageItems = hasMore ? artworks.slice(0, take) : artworks;
|
||||||
|
|
||||||
|
const nextCursor = hasMore ? pageItems[pageItems.length - 1]?.id : null;
|
||||||
|
|
||||||
|
return { items: pageItems, nextCursor };
|
||||||
|
}
|
||||||
22
src/lib/s3.ts
Normal file
22
src/lib/s3.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||||
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
|
import "dotenv/config";
|
||||||
|
|
||||||
|
export const s3 = new S3Client({
|
||||||
|
region: "us-east-1",
|
||||||
|
endpoint: `${process.env.S3_ENDPOINT}`,
|
||||||
|
forcePathStyle: true,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: `${process.env.S3_ACCESSKEY}`,
|
||||||
|
secretAccessKey: `${process.env.S3_SECRET}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getSignedImageUrl(key: string, expiresInSec = 3600) {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: `${process.env.BUCKET_NAME}`,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
return getSignedUrl(s3, command, { expiresIn: expiresInSec });
|
||||||
|
}
|
||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
9
src/schemas/artworks/albumSchema.ts
Normal file
9
src/schemas/artworks/albumSchema.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod/v4"
|
||||||
|
|
||||||
|
export const albumSchema = z.object({
|
||||||
|
name: z.string().min(3, "Name is required. Min 3 characters."),
|
||||||
|
slug: z.string().min(3, "Slug is required. Min 3 characters.").regex(/^[a-z]+$/, "Only lowercase letters are allowed (no numbers, spaces, or uppercase)"),
|
||||||
|
description: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type albumSchema = z.infer<typeof albumSchema>
|
||||||
9
src/schemas/artworks/categorySchema.ts
Normal file
9
src/schemas/artworks/categorySchema.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod/v4"
|
||||||
|
|
||||||
|
export const categorySchema = z.object({
|
||||||
|
name: z.string().min(3, "Name is required. Min 3 characters."),
|
||||||
|
slug: z.string().min(3, "Slug is required. Min 3 characters.").regex(/^[a-z]+$/, "Only lowercase letters are allowed (no numbers, spaces, or uppercase)"),
|
||||||
|
description: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type categorySchema = z.infer<typeof categorySchema>
|
||||||
47
src/schemas/artworks/imageSchema.ts
Normal file
47
src/schemas/artworks/imageSchema.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const fileUploadSchema = z.object({
|
||||||
|
file: z
|
||||||
|
.custom<FileList>()
|
||||||
|
.refine((files) => files instanceof FileList && files.length > 0, {
|
||||||
|
message: "Image file is required",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFileList = (v: unknown): v is FileList =>
|
||||||
|
typeof FileList !== "undefined" && v instanceof FileList;
|
||||||
|
|
||||||
|
export const fileBulkUploadSchema = z.object({
|
||||||
|
file: z
|
||||||
|
.custom<FileList>((v) => isFileList(v), "Please choose one or more files")
|
||||||
|
.refine((files) => files.length > 0, {
|
||||||
|
message: "At least one image file is required",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const artworkSchema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
needsWork: z.boolean(),
|
||||||
|
nsfw: z.boolean(),
|
||||||
|
published: z.boolean(),
|
||||||
|
setAsHeader: z.boolean(),
|
||||||
|
|
||||||
|
altText: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
month: z.number().optional(),
|
||||||
|
year: z.number().optional(),
|
||||||
|
creationDate: z.date().optional(),
|
||||||
|
|
||||||
|
// fileId: z.string(),
|
||||||
|
// albumId: z.string().optional(),
|
||||||
|
// typeId: z.string().optional(),
|
||||||
|
|
||||||
|
metadataId: z.string().optional(),
|
||||||
|
|
||||||
|
categoryIds: z.array(z.string()).optional(),
|
||||||
|
colorIds: z.array(z.string()).optional(),
|
||||||
|
// sortContextIds: z.array(z.string()).optional(),
|
||||||
|
tagIds: z.array(z.string()).optional(),
|
||||||
|
variantIds: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
10
src/schemas/artworks/tagSchema.ts
Normal file
10
src/schemas/artworks/tagSchema.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { z } from "zod/v4"
|
||||||
|
|
||||||
|
export const tagSchema = z.object({
|
||||||
|
name: z.string().min(3, "Name is required. Min 3 characters."),
|
||||||
|
slug: z.string().min(3, "Slug is required. Min 3 characters.").regex(/^[a-z]+$/, "Only lowercase letters are allowed (no numbers, spaces, or uppercase)"),
|
||||||
|
description: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type tagSchema = z.infer<typeof tagSchema>
|
||||||
|
|
||||||
9
src/schemas/artworks/typeSchema.ts
Normal file
9
src/schemas/artworks/typeSchema.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod/v4"
|
||||||
|
|
||||||
|
export const typeSchema = z.object({
|
||||||
|
name: z.string().min(3, "Name is required. Min 3 characters."),
|
||||||
|
slug: z.string().min(3, "Slug is required. Min 3 characters.").regex(/^[a-z]+$/, "Only lowercase letters are allowed (no numbers, spaces, or uppercase)"),
|
||||||
|
description: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type typeSchema = z.infer<typeof typeSchema>
|
||||||
14
src/types/Artwork.ts
Normal file
14
src/types/Artwork.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Prisma } from "@/generated/prisma/client";
|
||||||
|
|
||||||
|
export type ArtworkWithRelations = Prisma.ArtworkGetPayload<{
|
||||||
|
include: {
|
||||||
|
file: true;
|
||||||
|
gallery: true;
|
||||||
|
metadata: true;
|
||||||
|
albums: true;
|
||||||
|
categories: true;
|
||||||
|
colors: true;
|
||||||
|
tags: true;
|
||||||
|
variants: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
4
src/types/VibrantSwatch.ts
Normal file
4
src/types/VibrantSwatch.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type VibrantSwatch = {
|
||||||
|
_rgb: [number, number, number];
|
||||||
|
hex?: string;
|
||||||
|
};
|
||||||
9
src/utils/formatFileSize.ts
Normal file
9
src/utils/formatFileSize.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
const kb = bytes / 1024;
|
||||||
|
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
||||||
|
const mb = kb / 1024;
|
||||||
|
if (mb < 1024) return `${mb.toFixed(1)} MB`;
|
||||||
|
const gb = mb / 1024;
|
||||||
|
return `${gb.toFixed(2)} GB`;
|
||||||
|
}
|
||||||
22
src/utils/getImageBufferFromS3.ts
Normal file
22
src/utils/getImageBufferFromS3.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { s3 } from "@/lib/s3";
|
||||||
|
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import { Readable } from "stream";
|
||||||
|
|
||||||
|
export async function getImageBufferFromS3(fileKey: string, fileType?: string): Promise<Buffer> {
|
||||||
|
// const type = fileType ? fileType.split("/")[1] : "webp";
|
||||||
|
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: "gaertan",
|
||||||
|
Key: `original/${fileKey}.${fileType}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await s3.send(command);
|
||||||
|
const stream = response.Body as Readable;
|
||||||
|
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
}
|
||||||
96
src/utils/uploadHelper.ts
Normal file
96
src/utils/uploadHelper.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
// export function generatePaletteName(tones: Tone[]): string {
|
||||||
|
// const hexString = tones.map(t => t.hex.toLowerCase()).join('');
|
||||||
|
// const hash = crypto.createHash('sha256').update(hexString).digest('hex');
|
||||||
|
// return `palette-${hash.slice(0, 8)}`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function generateColorName(hex: string): string {
|
||||||
|
const hash = crypto.createHash("sha256").update(hex.toLowerCase()).digest("hex");
|
||||||
|
return `color-${hash.slice(0, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function generateExtractColorName(hex: string, hue?: number, sat?: number, area?: number): string {
|
||||||
|
// const data = `${hex.toLowerCase()}-${hue ?? 0}-${sat ?? 0}-${area ?? 0}`;
|
||||||
|
// const hash = crypto.createHash("sha256").update(data).digest("hex");
|
||||||
|
// return `extract-${hash.slice(0, 8)}`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function rgbToHex(rgb: number[]): string {
|
||||||
|
return `#${rgb
|
||||||
|
.map((val) => Math.round(val).toString(16).padStart(2, "0"))
|
||||||
|
.join("")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function argbToHex(argb: number): string {
|
||||||
|
return `#${argb.toString(16).slice(2).padStart(6, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function extractPaletteTones(
|
||||||
|
// palette: TonalPalette, tones: number[] = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
|
||||||
|
// ) {
|
||||||
|
// return tones.map((t) => ({
|
||||||
|
// tone: t,
|
||||||
|
// hex: argbToHex(palette.tone(t)),
|
||||||
|
// }));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export async function upsertPalettes(tones: Tone[], imageId: string, type: string) {
|
||||||
|
// const paletteName = generatePaletteName(tones);
|
||||||
|
|
||||||
|
// const existingPalette = await prisma.colorPalette.findFirst({
|
||||||
|
// where: { name: paletteName },
|
||||||
|
// include: { items: true },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// //
|
||||||
|
// const palette = existingPalette ?? await prisma.colorPalette.create({
|
||||||
|
// data: {
|
||||||
|
// name: paletteName,
|
||||||
|
// items: {
|
||||||
|
// create: tones.map(tone => ({
|
||||||
|
// tone: tone.tone,
|
||||||
|
// hex: tone.hex,
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await prisma.imagePalette.upsert({
|
||||||
|
// where: {
|
||||||
|
// imageId_type: {
|
||||||
|
// imageId,
|
||||||
|
// type,
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// update: {
|
||||||
|
// paletteId: palette.id
|
||||||
|
// },
|
||||||
|
// create: {
|
||||||
|
// imageId,
|
||||||
|
// paletteId: palette.id,
|
||||||
|
// type,
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
// // const newPalette = await prisma.colorPalette.create({
|
||||||
|
// // data: {
|
||||||
|
// // name: paletteName,
|
||||||
|
// // type: type,
|
||||||
|
// // items: {
|
||||||
|
// // create: tones.map(t => ({
|
||||||
|
// // tone: t.tone,
|
||||||
|
// // hex: t.hex,
|
||||||
|
// // })),
|
||||||
|
// // },
|
||||||
|
// // images: {
|
||||||
|
// // connect: { id: imageId },
|
||||||
|
// // },
|
||||||
|
// // },
|
||||||
|
// // include: { items: true },
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// return palette;
|
||||||
|
// }
|
||||||
Reference in New Issue
Block a user