mirror of
https://github.com/get-drexa/drive.git
synced 2025-11-30 21:41:39 +00:00
Compare commits
33 Commits
feat/keybo
...
83a5f92506
| Author | SHA1 | Date | |
|---|---|---|---|
|
83a5f92506
|
|||
|
b802cb5aec
|
|||
|
49b76934b2
|
|||
|
2ed8be94f1
|
|||
|
b17de812b9
|
|||
|
0c7e4c43e7
|
|||
|
03d36a2c80
|
|||
|
5eff2fa756
|
|||
|
0e460370da
|
|||
|
bcc0f9f5e2
|
|||
|
bf2087cded
|
|||
|
f7bc5fd958
|
|||
|
1fcdaf4f86
|
|||
|
483aa19351
|
|||
|
b654f50ddd
|
|||
|
33b235517c
|
|||
|
4978a173a8
|
|||
|
4686744fd0
|
|||
|
b745ad273e
|
|||
|
7c29f642f1
|
|||
|
6eef4d9c30
|
|||
|
9149243e95
|
|||
|
b43a88c6fc
|
|||
|
94b35df0e5
|
|||
|
19e52feebb
|
|||
|
e806d442b7
|
|||
|
57369d10fe
|
|||
|
9282e75bef
|
|||
|
875aae74e8
|
|||
|
c2d9010508
|
|||
|
0e686a1f85
|
|||
|
1d8a117b93
|
|||
|
022f3c4726
|
@@ -1,10 +1,8 @@
|
||||
CONVEX_SELF_HOSTED_URL=
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY=
|
||||
CONVEX_URL=
|
||||
WORKOS_CLIENT_ID=
|
||||
WORKOS_CLIENT_SECRET=
|
||||
WORKOS_API_KEY=
|
||||
|
||||
# this is the url to the convex instance
|
||||
BUN_PUBLIC_CONVEX_URL=
|
||||
BUN_PUBLIC_WORKOS_CLIENT_ID=
|
||||
BUN_PUBLIC_WORKOS_REDIRECT_URI=
|
||||
# this is the convex url for invoking http actions
|
||||
BUN_PUBLIC_CONVEX_SITE_URL=
|
||||
|
||||
393
bun.lock
393
bun.lock
@@ -2,7 +2,7 @@
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "bun-react-template",
|
||||
"name": "fileone",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@types/bun": "latest",
|
||||
@@ -15,6 +15,8 @@
|
||||
"@fileone/path": "workspace:*",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@convex-dev/better-auth": "^0.8.9",
|
||||
"better-auth": "1.3.8",
|
||||
"convex": "^1.27.0",
|
||||
"typescript": "^5",
|
||||
},
|
||||
@@ -29,12 +31,14 @@
|
||||
"name": "@fileone/web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@convex-dev/workos": "^0.0.1",
|
||||
"@convex-dev/better-auth": "^0.8.9",
|
||||
"@fileone/convex": "workspace:*",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
@@ -42,15 +46,18 @@
|
||||
"@tanstack/react-router": "^1.131.41",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/router-devtools": "^1.131.42",
|
||||
"@workos-inc/authkit-react": "^0.12.0",
|
||||
"better-auth": "1.3.8",
|
||||
"bun-plugin-tailwind": "latest",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"convex": "^1.27.0",
|
||||
"convex-helpers": "^0.1.104",
|
||||
"jotai": "^2.14.0",
|
||||
"jotai-effect": "^2.1.3",
|
||||
"jotai-scope": "^0.9.5",
|
||||
"lucide-react": "^0.544.0",
|
||||
"motion": "^12.23.16",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
@@ -123,6 +130,10 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
|
||||
"@better-auth/utils": ["@better-auth/utils@0.2.6", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-3y/vaL5Ox33dBwgJ6ub3OPkVqr6B5xL2kgxNHG8eHZuryLyG/4JSPGqjbdRSgjuy9kALUZYDFl+ORIAxlWMSuA=="],
|
||||
|
||||
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
|
||||
@@ -141,13 +152,7 @@
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
|
||||
|
||||
"@convex-dev/workos": ["@convex-dev/workos@0.0.1", "", { "dependencies": { "@workos-inc/authkit-react": "^0.11.0", "tsdown": "^0.12.7", "vitest": "^3.1.4" }, "peerDependencies": { "convex": "^1.25.4", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" } }, "sha512-8gZOgmcTitcKXwagdU69XC4Va6wMPFIhSqSOEaXmFXMEPtkMgxPW1dhJzrmm9UQ4iRgZsckjd2O5aQjUH7kHGQ=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
"@convex-dev/better-auth": ["@convex-dev/better-auth@0.8.9", "", { "dependencies": { "@better-fetch/fetch": "^1.1.18", "common-tags": "^1.8.2", "convex-helpers": "^0.1.95", "is-network-error": "^1.1.0", "type-fest": "^4.39.1", "zod": "^3.24.4" }, "peerDependencies": { "better-auth": "1.3.8", "convex": "^1.26.2", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-t6x2lYsgv0sGL14xmIsTqxVltkS//mWtIjb+Wm39rWUCgeqmCTqsyhQnmTDQNwaqZS0sH0WfTObNHu3xbCSx1w=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
|
||||
@@ -213,6 +218,8 @@
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||
|
||||
"@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -223,13 +230,63 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
|
||||
|
||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.71.0", "", {}, "sha512-QwoF5WUXIGFQ+hSxWEib4U/aeLoiDN9JlP18MnBgx9LLPRDfn1iICtcow7Jgey6HLH4XFceWXQD5WBJ39dyJcw=="],
|
||||
"@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.71.0", "", {}, "sha512-5CwQ4MI+P4MQbjLWXgNurA+igGwu/opNetIE13LBs9+V93R64MLvDKOOLZIXSzEfovU3Zef3q3GjPnMTgJTn2w=="],
|
||||
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"@quansync/fs": ["@quansync/fs@0.1.5", "", { "dependencies": { "quansync": "^0.2.11" } }, "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA=="],
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WeXSaL29ylJEZMYHHW28QZ6rgAbxQ1KuNSZD9gvd3fPlo0s6s2PglvPArjjP07nmvIK9m4OffN0k4M98O7WmAg=="],
|
||||
|
||||
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CFKjoUWQH0Oz3UHYfKbdKLq0wGryrFsTJEYq839qAwHQSECvVZYAnxVVDYUDa0yQFonhO2qSHY41f6HK+b7xtw=="],
|
||||
|
||||
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+FSr/ub5vA/EkD3fMhHJUzYioSf/sXd50OGxNDAntVxcDu4tXL/81Ka3R/gkZmjznpLFIzovU/1Ts+b7dlkrfw=="],
|
||||
|
||||
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WHthS/eLkCNcp9pk4W8aubRl9fIUgt2XhHyLrP0GClB1FVvmodu/zIOtG0NXNpzlzB8+gglOkGo4dPjfVf4Z+g=="],
|
||||
|
||||
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HT5sr7N8NDYbQRjAnT7ISpx64y+ewZZRQozOJb0+KQObKvg4UUNXGm4Pn1xA4/WPMZDDazjO8E2vtOQw1nJlAQ=="],
|
||||
|
||||
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-sGEWoJQXO4GDr0x4t/yJQ/Bq1yNkOdX9tHbZZ+DBGJt3z3r7jeb4Digv8xQUk6gdTFC9vnGHuin+KW3/yD1Aww=="],
|
||||
|
||||
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-OmlEH3nlxQyv7HOvTH21vyNAZGv9DIPnrTznzvKiOQxkOphhCyKvPTlF13ydw4s/i18iwaUrhHy+YG9HSSxa4Q=="],
|
||||
|
||||
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rtzUEzCynl3Rhgn/iR9DQezSFiZMcAXAbU+xfROqsweMGKwvwIA2ckyyckO08psEP8XcUZTs3LT9CH7PnaMiEA=="],
|
||||
|
||||
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hrr7mDvUjMX1tuJaXz448tMsgKIqGJBY8+rJqztKOw1U5+a/v2w5HuIIW1ce7ut0ZwEn+KIDvAujlPvpH33vpQ=="],
|
||||
|
||||
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xXwtpZVVP7T+vkxcF/TUVVOGRjEfkByO4mKveKYb4xnHWV4u4NnV0oNmzyMKkvmj10to5j2h0oZxA4ZVVv4gfA=="],
|
||||
|
||||
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/jVZ8eYjpYHLDFNoT86cP+AjuWvpkzFY+0R0a1bdeu0sQ6ILuy1FV6hz1hUAP390E09VCo5oP76fnx29giHTtA=="],
|
||||
|
||||
"@peculiar/asn1-android": ["@peculiar/asn1-android@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A=="],
|
||||
|
||||
"@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A=="],
|
||||
|
||||
"@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ=="],
|
||||
|
||||
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg=="],
|
||||
|
||||
"@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug=="],
|
||||
|
||||
"@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw=="],
|
||||
|
||||
"@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pfx": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A=="],
|
||||
|
||||
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q=="],
|
||||
|
||||
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.5.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ=="],
|
||||
|
||||
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ=="],
|
||||
|
||||
"@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A=="],
|
||||
|
||||
"@peculiar/x509": ["@peculiar/x509@1.14.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-csr": "^2.5.0", "@peculiar/asn1-ecc": "^2.5.0", "@peculiar/asn1-pkcs9": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||
|
||||
@@ -259,6 +316,8 @@
|
||||
|
||||
"@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.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-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||
|
||||
"@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-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=="],
|
||||
@@ -269,6 +328,8 @@
|
||||
|
||||
"@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-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@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-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
|
||||
|
||||
"@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-separator": ["@radix-ui/react-separator@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-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||
@@ -297,177 +358,129 @@
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Mp0/gqiPdepHjjVm7e0yL1acWvI0rJVVFQEADSezvAjon9sjQ7CEg9JnXICD4B1YrPmN9qV/e7cQZCp87tTV4w=="],
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "darwin", "cpu": "x64" }, "sha512-40re4rMNrsi57oavRzIOpRGmg3QRlW6Ea8Q3znaqgOuJuKVrrm2bIQInTfkZJG7a4/5YMX7T951d0+toGLTdCA=="],
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8BDM939bbMariZupiHp3OmP5N+LXPT4mULA0hZjDaq970PCxv4krZOSMG+HkWUUwmuQROtV+/00xw39EO0P+8g=="],
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm" }, "sha512-sntsPaPgrECpBB/+2xrQzVUt0r493TMPI+4kWRMhvMsmrxOqH1Ep5lM0Wua/ZdbfZNwm1aVa5pcESQfNfM4Fhw=="],
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5clBW/I+er9F2uM1OFjJFWX86y7Lcy0M+NqsN4s3o07W+8467Zk8oQa4B45vdaXoNUF/yqIAgKkA/OEdQDxZqA=="],
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "arm64" }, "sha512-wv+rnAfQDk9p/CheX8/Kmqk2o1WaFa4xhWI9gOyDMk/ljvOX0u0ubeM8nI1Qfox7Tnh71eV5AjzSePXUhFOyOg=="],
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "x64" }, "sha512-gxD0/xhU4Py47IH3bKZbWtvB99tMkUPGPJFRfSc5UB9Osoje0l0j1PPbxpUtXIELurYCqwLBKXIMTQGifox1BQ=="],
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.9-commit.d91dfb5", "", { "os": "linux", "cpu": "x64" }, "sha512-HotuVe3XUjDwqqEMbm3o3IRkP9gdm8raY/btd/6KE3JGLF/cv4+3ff1l6nOhAZI8wulWDPEXPtE7v+HQEaTXnA=="],
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.9-commit.d91dfb5", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.4" }, "cpu": "none" }, "sha512-8Cx+ucbd8n2dIr21FqBh6rUvTVL0uTgEtKR7l+MUZ5BgY4dFh1e4mPVX8oqmoYwOxBiXrsD2JIOCz4AyKLKxWA=="],
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Vhq5vikrVDxAa75fxsyqj0c0Y/uti/TwshXI71Xb8IeUQJOBnmLUsn5dgYf5ljpYYkNa0z9BPAvUDIDMmyDi+w=="],
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="],
|
||||
|
||||
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "ia32" }, "sha512-lN7RIg9Iugn08zP2aZN9y/MIdG8iOOCE93M1UrFlrxMTqPf8X+fDzmR/OKhTSd1A2pYNipZHjyTcb5H8kyQSow=="],
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.9-commit.d91dfb5", "", { "os": "win32", "cpu": "x64" }, "sha512-7/7cLIn48Y+EpQ4CePvf8reFl63F15yPUlg4ZAhl+RXJIfydkdak1WD8Ir3AwAO+bJBXzrfNL+XQbxm0mcQZmw=="],
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5", "", {}, "sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ=="],
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.1", "", { "os": "android", "cpu": "arm" }, "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag=="],
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.1", "", { "os": "android", "cpu": "arm64" }, "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw=="],
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw=="],
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw=="],
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA=="],
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ=="],
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.1", "", { "os": "linux", "cpu": "arm" }, "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg=="],
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.1", "", { "os": "linux", "cpu": "arm" }, "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw=="],
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw=="],
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w=="],
|
||||
"@simplewebauthn/browser": ["@simplewebauthn/browser@13.2.2", "", {}, "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q=="],
|
||||
"@simplewebauthn/server": ["@simplewebauthn/server@13.2.2", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.13.0" } }, "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q=="],
|
||||
"@tanstack/history": ["@tanstack/history@1.132.31", "", {}, "sha512-UCHM2uS0t/uSszqPEo+SBSSoQVeQ+LlOWAVBl5SA7+AedeAbKafIPjFn8huZCXNLAYb0WKV2+wETr7lDK9uz7g=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ=="],
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg=="],
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.2", "", { "dependencies": { "@tanstack/query-core": "5.90.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg=="],
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.132.37", "", { "dependencies": { "@tanstack/history": "1.132.31", "@tanstack/react-store": "^0.7.0", "@tanstack/router-core": "1.132.37", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-HwF9ZIkPt1AQnj18t1z+T8iB/gF4ldG5Pk9vDWcUbt8luWzBJOyOkTJGgbMpviuLEP79LBwEsXuWZ964upNd8g=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA=="],
|
||||
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.132.37", "", { "dependencies": { "@tanstack/router-devtools-core": "1.132.37", "vite": "^7.1.7" }, "peerDependencies": { "@tanstack/react-router": "^1.132.37", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-6zKjnB1gTfM1UZiEe2I4bSIxxJ0Z1xC57R02l6ogRzyaqEimMIUFv873earTiWNlWwvKVuiBrwgE5/hsivgqNg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.1", "", { "os": "linux", "cpu": "x64" }, "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.1", "", { "os": "none", "cpu": "arm64" }, "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.1", "", { "os": "win32", "cpu": "x64" }, "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA=="],
|
||||
|
||||
"@tanstack/history": ["@tanstack/history@1.131.2", "", {}, "sha512-cs1WKawpXIe+vSTeiZUuSBy8JFjEuDgdMKZFRLKwQysKo8y2q6Q1HvS74Yw+m5IhOW1nTZooa6rlgdfXcgFAaw=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.87.4", "", {}, "sha512-uNsg6zMxraEPDVO2Bn+F3/ctHi+Zsk+MMpcN8h6P7ozqD088F6mFY5TfGM7zuyIrL7HKpDyu6QHfLWiDxh3cuw=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.87.4", "", { "dependencies": { "@tanstack/query-core": "5.87.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-T5GT/1ZaNsUXf5I3RhcYuT17I4CPlbZgyLxc/ZGv7ciS6esytlbjb3DgUFO6c8JWYMDpdjSWInyGZUErgzqhcA=="],
|
||||
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.131.41", "", { "dependencies": { "@tanstack/history": "1.131.2", "@tanstack/react-store": "^0.7.0", "@tanstack/router-core": "1.131.41", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-QEbTYpAosiD8e4qEZRr9aJipGSb8pQc+pfZwK6NCD2Tcxwu2oF6MVtwv0bIDLRpZP0VJMBpxXlTRISUDNMNqIA=="],
|
||||
|
||||
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.131.42", "", { "dependencies": { "@tanstack/router-devtools-core": "1.131.42" }, "peerDependencies": { "@tanstack/react-router": "^1.131.41", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-7pymFB1CCimRHot2Zp0ZekQjd1iN812V88n9NLPSeiv9sVRtRVIaLphJjDeudx1NNgkfSJPx2lOhz6K38cuZog=="],
|
||||
|
||||
"@tanstack/react-store": ["@tanstack/react-store@0.7.5", "", { "dependencies": { "@tanstack/store": "0.7.5", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-A+WZtEnHZpvbKXm8qR+xndNKywBLez2KKKKEQc7w0Qs45GvY1LpRI3BTZNmELwEVim8+Apf99iEDH2J+MUIzlQ=="],
|
||||
"@tanstack/react-store": ["@tanstack/react-store@0.7.7", "", { "dependencies": { "@tanstack/store": "0.7.7", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg=="],
|
||||
|
||||
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
|
||||
|
||||
"@tanstack/router-cli": ["@tanstack/router-cli@1.131.41", "", { "dependencies": { "@tanstack/router-generator": "1.131.41", "chokidar": "^3.6.0", "yargs": "^17.7.2" }, "bin": { "tsr": "bin/tsr.cjs" } }, "sha512-EpLnnCwwCd94HRCWHoa1GZGtIWIffx4rPBb6gbWm4cvyEIGV2Gq+27vL2OEw819/elxyBQmG2RrPB8+7dfVACw=="],
|
||||
"@tanstack/router-cli": ["@tanstack/router-cli@1.132.37", "", { "dependencies": { "@tanstack/router-generator": "1.132.37", "chokidar": "^3.6.0", "yargs": "^17.7.2" }, "bin": { "tsr": "bin/tsr.cjs" } }, "sha512-dc7156OyQEFgn1pS3t8BGEtkfpXex0hOCTbeOqMISDT2Jl7kQkx9xJwmEUdp990Kyy4eKgr9+gYQgP8iwepfJw=="],
|
||||
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.131.41", "", { "dependencies": { "@tanstack/history": "1.131.2", "@tanstack/store": "^0.7.0", "cookie-es": "^1.2.2", "seroval": "^1.3.2", "seroval-plugins": "^1.3.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-VoLly00DWM0abKuVPRm8wiwGtRBHOKs6K896fy48Q/KYoDVLs8kRCRjFGS7rGnYC2FIkmmvHqYRqNg7jgCx2yg=="],
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.132.37", "", { "dependencies": { "@tanstack/history": "1.132.31", "@tanstack/store": "^0.7.0", "cookie-es": "^2.0.0", "seroval": "^1.3.2", "seroval-plugins": "^1.3.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-7I+1qGY6uAKwh6q5QsNU8+DXDbnXfobizHIDo8ff+fkiMy9l00eGBCUQRSZa0KuE9kJAPHuCxtB/MLxsHNEoQg=="],
|
||||
|
||||
"@tanstack/router-devtools": ["@tanstack/router-devtools@1.131.42", "", { "dependencies": { "@tanstack/react-router-devtools": "1.131.42", "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/react-router": "^1.131.41", "csstype": "^3.0.10", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["csstype"] }, "sha512-iWJzr4aN/IOsDSaF/kysM7tPSYj89hnzcWMKNuYN9redIwHgg7rNZ4toKhfNWYNfzxdhKwL9/Yvpf7bDemyc+Q=="],
|
||||
"@tanstack/router-devtools": ["@tanstack/router-devtools@1.132.37", "", { "dependencies": { "@tanstack/react-router-devtools": "1.132.37", "clsx": "^2.1.1", "goober": "^2.1.16", "vite": "^7.1.7" }, "peerDependencies": { "@tanstack/react-router": "^1.132.37", "csstype": "^3.0.10", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["csstype"] }, "sha512-EHI5DWm+XBGZ3ENXjyVMyUk+5GSovIXI9fTPiy516P3G+DciTpThHhm6TtFr4AhlFjFX1vBLguoASsXWFNgyRQ=="],
|
||||
|
||||
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.131.42", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.5" }, "peerDependencies": { "@tanstack/router-core": "^1.131.41", "csstype": "^3.0.10", "tiny-invariant": "^1.3.3" }, "optionalPeers": ["csstype"] }, "sha512-o8jKTiwXcUSjmkozcMjIw1yhjVYeXcuQO7DtfgjKW3B85iveH6VzYK+bGEVU7wmLNMuUSe2eI/7RBzJ6a5+MCA=="],
|
||||
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.132.37", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.5", "vite": "^7.1.7" }, "peerDependencies": { "@tanstack/router-core": "^1.132.37", "csstype": "^3.0.10", "tiny-invariant": "^1.3.3" }, "optionalPeers": ["csstype"] }, "sha512-/faZqU6zrFpwNlpWlFgjOnrpKpN7s23kGtz7HLXXOnqEBPO+GdayxMszwG1q9Yv47YIRjk1CWmAnjM9q6cQ6pQ=="],
|
||||
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.131.41", "", { "dependencies": { "@tanstack/router-core": "1.131.41", "@tanstack/router-utils": "1.131.2", "@tanstack/virtual-file-routes": "1.131.2", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-HsDkBU1u/KvHrzn76v/9oeyMFuxvVlE3dfIu4fldZbPy/i903DWBwODIDGe6fVUsYtzPPrRvNtbjV18HVz5GCA=="],
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.132.37", "", { "dependencies": { "@tanstack/router-core": "1.132.37", "@tanstack/router-utils": "1.132.31", "@tanstack/virtual-file-routes": "1.132.31", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-bR+wfGOnHIZdzx7FSktqpnYf9McBDwLiaV6HFKu4Mvlg5KXZEOJDBfW3HX0oURpEMGJfcaRdMY8n5/lTFx3v5A=="],
|
||||
|
||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.131.2", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2" } }, "sha512-sr3x0d2sx9YIJoVth0QnfEcAcl+39sQYaNQxThtHmRpyeFYNyM2TTH+Ud3TNEnI3bbzmLYEUD+7YqB987GzhDA=="],
|
||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.132.31", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "fast-glob": "^3.3.3", "pathe": "^2.0.3" } }, "sha512-uf8mQ3wV58K8TL5XXBoWhkYxmCV7LLWbbf6AvcxdhnCnBNmXBGlY+T8RdsRnXyI2Iyp2HfHaVZ+8H3CEQedXfw=="],
|
||||
|
||||
"@tanstack/store": ["@tanstack/store@0.7.5", "", {}, "sha512-qd/OjkjaFRKqKU4Yjipaen/EOB9MyEg6Wr9fW103RBPACf1ZcKhbhcu2S5mj5IgdPib6xFIgCUti/mKVkl+fRw=="],
|
||||
"@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="],
|
||||
|
||||
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
||||
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.131.2", "", {}, "sha512-VEEOxc4mvyu67O+Bl0APtYjwcNRcL9it9B4HKbNgcBTIOEalhk+ufBl4kiqc8WP1sx1+NAaiS+3CcJBhrqaSRg=="],
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.132.31", "", {}, "sha512-rxS8Cm2nIXroLqkm9pE/8X2lFNuvcTIIiFi5VH4PwzvKscAuaW3YRMN1WmaGDI2mVEn+GLaoY6Kc3jOczL5i4w=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
"@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/node": ["@types/node@24.3.3", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw=="],
|
||||
"@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
|
||||
"@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
|
||||
|
||||
"@workos-inc/authkit-js": ["@workos-inc/authkit-js@0.13.0", "", {}, "sha512-iA0Dt7D1BmY2/1s4oeA36W/aRt8/b5iyH6rP4AlgnjrcH2lUGkBgDXL76NXc0M7repkDQTMcJJ2NhCSo2rcWmg=="],
|
||||
|
||||
"@workos-inc/authkit-react": ["@workos-inc/authkit-react@0.12.0", "", { "dependencies": { "@workos-inc/authkit-js": "0.13.0" }, "peerDependencies": { "react": ">=17" } }, "sha512-j3OckFxz3iDeheRHMWBCIVDAUhSw/hjBFEjKS9U3azwjnbRGb/x4CHmGa4pdNtq+trf2NrlISC648Nwz6fJdGg=="],
|
||||
"@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="],
|
||||
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"ast-kit": ["ast-kit@2.1.2", "", { "dependencies": { "@babel/parser": "^7.28.0", "pathe": "^2.0.3" } }, "sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g=="],
|
||||
"asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
|
||||
|
||||
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.3", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ=="],
|
||||
|
||||
"better-auth": ["better-auth@1.3.8", "", { "dependencies": { "@better-auth/utils": "0.2.6", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.8.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.16", "defu": "^6.1.4", "jose": "^5.10.0", "kysely": "^0.28.5", "nanostores": "^0.11.4", "zod": "^4.1.5" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-uRFzHbWkhr8eWNy+BJwyMnrZPOvQjwrcLND3nc6jusRteYA9cjeRGElgCPTWTIyWUfzaQ708Lb5Mdq9Gv41Qpw=="],
|
||||
|
||||
"better-call": ["better-call@1.0.16", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-42dgJ1rOtc0anOoxjXPOWuel/Z/4aeO7EJ2SiXNwvlkySSgjXhNjAjTMWa8DL1nt6EXS3jl3VKC3mPsU/lUgVA=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"birpc": ["birpc@2.5.0", "", {}, "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.26.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.2", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A=="],
|
||||
"browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="],
|
||||
|
||||
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="],
|
||||
"bun": ["bun@1.3.0", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.0", "@oven/bun-darwin-x64": "1.3.0", "@oven/bun-darwin-x64-baseline": "1.3.0", "@oven/bun-linux-aarch64": "1.3.0", "@oven/bun-linux-aarch64-musl": "1.3.0", "@oven/bun-linux-x64": "1.3.0", "@oven/bun-linux-x64-baseline": "1.3.0", "@oven/bun-linux-x64-musl": "1.3.0", "@oven/bun-linux-x64-musl-baseline": "1.3.0", "@oven/bun-windows-x64": "1.3.0", "@oven/bun-windows-x64-baseline": "1.3.0" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-YI7mFs7iWc/VsGsh2aw6eAPD2cjzn1j+LKdYVk09x1CrdTWKYIHyd+dG5iQoN9//3hCDoZj8U6vKpZzEf5UARA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
||||
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001741", "", {}, "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw=="],
|
||||
|
||||
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
||||
|
||||
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001747", "", {}, "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg=="],
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
@@ -481,51 +494,45 @@
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"convex": ["convex@1.27.0", "", { "dependencies": { "esbuild": "0.25.4", "jwt-decode": "^4.0.0", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-IHkqZX3GtY4nKFPTAR4mvWHHhDiQX9PM7EjpEv0pJWoMoq0On6oOL3iZ7Xz4Ls96dF7WJd4AjfitJsg2hUnLSQ=="],
|
||||
"convex": ["convex@1.27.3", "", { "dependencies": { "esbuild": "0.25.4", "jwt-decode": "^4.0.0", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-Ebr9lPgXkL7JO5IFr3bG+gYvHskyJjc96Fx0BBNkJUDXrR/bd9/uI4q8QszbglK75XfDu068vR0d/HK2T7tB9Q=="],
|
||||
|
||||
"convex-helpers": ["convex-helpers@0.1.104", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.24.0", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.22.4 || ^4.0.15" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-7CYvx7T3K6n+McDTK4ZQaQNNGBzq5aWezpjzsKbOxPXx7oNcTP9wrpef3JxeXWFzkByJv5hRCjseh9B7eNJ7Ig=="],
|
||||
|
||||
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
|
||||
"cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||
|
||||
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
||||
|
||||
"dts-resolver": ["dts-resolver@2.1.2", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.218", "", {}, "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.230", "", {}, "sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"framer-motion": ["framer-motion@12.23.16", "", { "dependencies": { "motion-dom": "^12.23.12", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N81A8hiHqVsexOzI3wzkibyLURW1nEJsZaRuctPhG4AdbbciYu+bKJq9I2lQFzAO4Bx3h4swI6pBbF/Hu7f7BA=="],
|
||||
"framer-motion": ["framer-motion@12.23.22", "", { "dependencies": { "motion-dom": "^12.23.21", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
@@ -539,9 +546,7 @@
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"goober": ["goober@2.1.16", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
"goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="],
|
||||
|
||||
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||
|
||||
@@ -551,13 +556,19 @@
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-network-error": ["is-network-error@1.3.0", "", {}, "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"isbot": ["isbot@5.1.30", "", {}, "sha512-3wVJEonAns1OETX83uWsk5IAne2S5zfDcntD2hbtU23LelSqNXzXs9zKjMPOLMzroCgIjCfjYAEHrd2D6FOkiA=="],
|
||||
"isbot": ["isbot@5.1.31", "", {}, "sha512-DPgQshehErHAqSCKDb3rNW03pa2wS/v5evvUqtxt6TTnHRqAG8FdzcSSJs9656pK6Y+NT7K9R4acEYXLHYfpUQ=="],
|
||||
|
||||
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
|
||||
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||
|
||||
"jotai": ["jotai@2.14.0", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-JQkNkTnqjk1BlSUjHfXi+pGG/573bVN104gp6CymhrWDseZGDReTNniWrLhJ+zXbM6pH+82+UNJ2vwYQUkQMWQ=="],
|
||||
"jotai": ["jotai@2.15.0", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw=="],
|
||||
|
||||
"jotai-effect": ["jotai-effect@2.1.3", "", { "peerDependencies": { "jotai": ">=2.14.0" } }, "sha512-gFIqKvW5hljRLaZihqI48SFFYZQifaT3ZDsqBdqyMRRvm++PAWpcmrRWvqG2MrG346Chs4QUWeZzAucgViggDQ=="],
|
||||
|
||||
"jotai-scope": ["jotai-scope@0.9.5", "", { "peerDependencies": { "jotai": ">=2.15.0", "react": ">=16.0.0" } }, "sha512-oOUduQ4ObALHz1+tAyoGeiuNTO3X3H8sUoOfliuMvQqS0HAhTHspFTq06b6SvKQkUtruw98XzVntsrGChmBRNA=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
@@ -567,34 +578,36 @@
|
||||
|
||||
"jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="],
|
||||
|
||||
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
||||
"kysely": ["kysely@0.28.7", "", {}, "sha512-u/cAuTL4DRIiO2/g4vNGRgklEKNIj5Q3CG7RoUB5DV5SfEC2hMvPxKi0GWPmnzwL2ryIeud2VTcEEmqzTzEPNw=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"motion": ["motion@12.23.16", "", { "dependencies": { "framer-motion": "^12.23.16", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-8vVuxZgcfGZm4kgSqFgGrhQ+6034y4UuEsqCX8s7UYeoQ+NO3R9LV5AyDlVr2Mb7xvS7ZM5s/XkTurWbWQ+UHA=="],
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"motion-dom": ["motion-dom@12.23.12", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw=="],
|
||||
"motion": ["motion@12.23.22", "", { "dependencies": { "framer-motion": "^12.23.22", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-iSq6X9vLHbeYwmHvhK//+U74ROaPnZmBuy60XZzqNl0QtZkWfoZyMDHYnpKuWFv0sNMqHgED8aCXk94LCoQPGg=="],
|
||||
|
||||
"motion-dom": ["motion-dom@12.23.21", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ=="],
|
||||
|
||||
"motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||
|
||||
"nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="],
|
||||
|
||||
"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-releases": ["node-releases@2.0.21", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="],
|
||||
"node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
@@ -603,11 +616,15 @@
|
||||
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
|
||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||
|
||||
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
|
||||
"pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "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-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
|
||||
|
||||
@@ -619,25 +636,29 @@
|
||||
|
||||
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||
|
||||
"reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-beta.9-commit.d91dfb5", "", { "dependencies": { "@oxc-project/runtime": "0.71.0", "@oxc-project/types": "0.71.0", "@rolldown/pluginutils": "1.0.0-beta.9-commit.d91dfb5", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-darwin-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-freebsd-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.9-commit.d91dfb5" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-FHkj6gGEiEgmAXQchglofvUUdwj2Oiw603Rs+zgFAnn9Cb7T7z3fiaEc0DbN3ja4wYkW6sF2rzMEtC1V4BGx/g=="],
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.13.14", "", { "dependencies": { "@babel/generator": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/types": "^7.28.1", "ast-kit": "^2.1.1", "birpc": "^2.5.0", "debug": "^4.4.1", "dts-resolver": "^2.1.1", "get-tsconfig": "^4.10.1" }, "peerDependencies": { "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.9", "typescript": "^5.0.0", "vue-tsc": "^2.2.0 || ^3.0.0" }, "optionalPeers": ["@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ=="],
|
||||
"rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="],
|
||||
|
||||
"rollup": ["rollup@4.50.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.1", "@rollup/rollup-android-arm64": "4.50.1", "@rollup/rollup-darwin-arm64": "4.50.1", "@rollup/rollup-darwin-x64": "4.50.1", "@rollup/rollup-freebsd-arm64": "4.50.1", "@rollup/rollup-freebsd-x64": "4.50.1", "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", "@rollup/rollup-linux-arm-musleabihf": "4.50.1", "@rollup/rollup-linux-arm64-gnu": "4.50.1", "@rollup/rollup-linux-arm64-musl": "4.50.1", "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", "@rollup/rollup-linux-ppc64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-musl": "4.50.1", "@rollup/rollup-linux-s390x-gnu": "4.50.1", "@rollup/rollup-linux-x64-gnu": "4.50.1", "@rollup/rollup-linux-x64-musl": "4.50.1", "@rollup/rollup-openharmony-arm64": "4.50.1", "@rollup/rollup-win32-arm64-msvc": "4.50.1", "@rollup/rollup-win32-ia32-msvc": "4.50.1", "@rollup/rollup-win32-x64-msvc": "4.50.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA=="],
|
||||
"rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
|
||||
|
||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="],
|
||||
|
||||
"seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
|
||||
|
||||
"solid-js": ["solid-js@1.9.9", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA=="],
|
||||
|
||||
@@ -647,51 +668,37 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="],
|
||||
"tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
|
||||
|
||||
"tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"tsdown": ["tsdown@0.12.9", "", { "dependencies": { "ansis": "^4.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "debug": "^4.4.1", "diff": "^8.0.2", "empathic": "^2.0.0", "hookable": "^5.5.3", "rolldown": "^1.0.0-beta.19", "rolldown-plugin-dts": "^0.13.12", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "unconfig": "^7.3.2" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsx": ["tsx@4.20.5", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw=="],
|
||||
"tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="],
|
||||
|
||||
"tw-animate-css": ["tw-animate-css@1.3.8", "", {}, "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw=="],
|
||||
"tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||
|
||||
"unconfig": ["unconfig@7.3.3", "", { "dependencies": { "@quansync/fs": "^0.1.5", "defu": "^6.1.4", "jiti": "^2.5.1", "quansync": "^0.2.11" } }, "sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA=="],
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
@@ -699,15 +706,9 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="],
|
||||
|
||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||
|
||||
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
"vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
@@ -721,26 +722,18 @@
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@convex-dev/workos/@workos-inc/authkit-react": ["@workos-inc/authkit-react@0.11.0", "", { "dependencies": { "@workos-inc/authkit-js": "0.13.0" }, "peerDependencies": { "react": ">=17" } }, "sha512-67HFSxP4wXC8ECGyvc1yGMwuD5NGkwT2OPt8DavHoKAlO+hRaAlu9wwzqUx1EJrHht0Dcx+l20Byq8Ab0bEhlg=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"better-auth/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"tsdown/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
|
||||
"tsdown/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
"tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
|
||||
}
|
||||
}
|
||||
|
||||
90
packages/convex/README.md
Normal file
90
packages/convex/README.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Welcome to your Convex functions directory!
|
||||
|
||||
Write your Convex functions here.
|
||||
See https://docs.convex.dev/functions for more.
|
||||
|
||||
A query function that takes two arguments looks like:
|
||||
|
||||
```ts
|
||||
// convex/myFunctions.ts
|
||||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const myQueryFunction = query({
|
||||
// Validators for arguments.
|
||||
args: {
|
||||
first: v.number(),
|
||||
second: v.string(),
|
||||
},
|
||||
|
||||
// Function implementation.
|
||||
handler: async (ctx, args) => {
|
||||
// Read the database as many times as you need here.
|
||||
// See https://docs.convex.dev/database/reading-data.
|
||||
const documents = await ctx.db.query("tablename").collect();
|
||||
|
||||
// Arguments passed from the client are properties of the args object.
|
||||
console.log(args.first, args.second);
|
||||
|
||||
// Write arbitrary JavaScript here: filter, aggregate, build derived data,
|
||||
// remove non-public properties, or create new objects.
|
||||
return documents;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Using this query function in a React component looks like:
|
||||
|
||||
```ts
|
||||
const data = useQuery(api.myFunctions.myQueryFunction, {
|
||||
first: 10,
|
||||
second: "hello",
|
||||
});
|
||||
```
|
||||
|
||||
A mutation function looks like:
|
||||
|
||||
```ts
|
||||
// convex/myFunctions.ts
|
||||
import { mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const myMutationFunction = mutation({
|
||||
// Validators for arguments.
|
||||
args: {
|
||||
first: v.string(),
|
||||
second: v.string(),
|
||||
},
|
||||
|
||||
// Function implementation.
|
||||
handler: async (ctx, args) => {
|
||||
// Insert or modify documents in the database here.
|
||||
// Mutations can also read from the database like queries.
|
||||
// See https://docs.convex.dev/database/writing-data.
|
||||
const message = { body: args.first, author: args.second };
|
||||
const id = await ctx.db.insert("messages", message);
|
||||
|
||||
// Optionally, return a value from your mutation.
|
||||
return await ctx.db.get(id);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Using this mutation function in a React component looks like:
|
||||
|
||||
```ts
|
||||
const mutation = useMutation(api.myFunctions.myMutationFunction);
|
||||
function handleButtonPress() {
|
||||
// fire and forget, the most common way to use mutations
|
||||
mutation({ first: "Hello!", second: "me" });
|
||||
// OR
|
||||
// use the result once the mutation has completed
|
||||
mutation({ first: "Hello!", second: "me" }).then((result) =>
|
||||
console.log(result),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Use the Convex CLI to push your functions to a deployment. See everything
|
||||
the Convex CLI can do by running `npx convex -h` in your project root
|
||||
directory. To learn more, launch the docs with `npx convex docs`.
|
||||
965
packages/convex/_generated/api.d.ts
vendored
965
packages/convex/_generated/api.d.ts
vendored
@@ -8,20 +8,29 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
FilterApi,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
import type * as auth from "../auth.js";
|
||||
import type * as betterauth__generated_api from "../betterauth/_generated/api.js";
|
||||
import type * as betterauth__generated_server from "../betterauth/_generated/server.js";
|
||||
import type * as betterauth_adapter from "../betterauth/adapter.js";
|
||||
import type * as betterauth_auth from "../betterauth/auth.js";
|
||||
import type * as convex__generated_api from "../convex/_generated/api.js";
|
||||
import type * as convex__generated_server from "../convex/_generated/server.js";
|
||||
import type * as files from "../files.js";
|
||||
import type * as filesystem from "../filesystem.js";
|
||||
import type * as functions from "../functions.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as model_directories from "../model/directories.js";
|
||||
import type * as model_error from "../model/error.js";
|
||||
import type * as model_files from "../model/files.js";
|
||||
import type * as model_filesystem from "../model/filesystem.js";
|
||||
import type * as model_user from "../model/user.js";
|
||||
import type * as users from "../users.js";
|
||||
import type * as user from "../user.js";
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
FilterApi,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
@@ -32,21 +41,959 @@ import type * as users from "../users.js";
|
||||
* ```
|
||||
*/
|
||||
declare const fullApi: ApiFromModules<{
|
||||
auth: typeof auth;
|
||||
"betterauth/_generated/api": typeof betterauth__generated_api;
|
||||
"betterauth/_generated/server": typeof betterauth__generated_server;
|
||||
"betterauth/adapter": typeof betterauth_adapter;
|
||||
"betterauth/auth": typeof betterauth_auth;
|
||||
"convex/_generated/api": typeof convex__generated_api;
|
||||
"convex/_generated/server": typeof convex__generated_server;
|
||||
files: typeof files;
|
||||
filesystem: typeof filesystem;
|
||||
functions: typeof functions;
|
||||
http: typeof http;
|
||||
"model/directories": typeof model_directories;
|
||||
"model/error": typeof model_error;
|
||||
"model/files": typeof model_files;
|
||||
"model/filesystem": typeof model_filesystem;
|
||||
"model/user": typeof model_user;
|
||||
users: typeof users;
|
||||
user: typeof user;
|
||||
}>;
|
||||
declare const fullApiWithMounts: typeof fullApi;
|
||||
|
||||
export declare const api: FilterApi<
|
||||
typeof fullApi,
|
||||
typeof fullApiWithMounts,
|
||||
FunctionReference<any, "public">
|
||||
>;
|
||||
export declare const internal: FilterApi<
|
||||
typeof fullApi,
|
||||
typeof fullApiWithMounts,
|
||||
FunctionReference<any, "internal">
|
||||
>;
|
||||
|
||||
export declare const components: {
|
||||
betterAuth: {
|
||||
adapter: {
|
||||
create: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{
|
||||
input:
|
||||
| {
|
||||
data: {
|
||||
createdAt: number;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
image?: null | string;
|
||||
name: string;
|
||||
updatedAt: number;
|
||||
userId?: null | string;
|
||||
};
|
||||
model: "user";
|
||||
}
|
||||
| {
|
||||
data: {
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
ipAddress?: null | string;
|
||||
token: string;
|
||||
updatedAt: number;
|
||||
userAgent?: null | string;
|
||||
userId: string;
|
||||
};
|
||||
model: "session";
|
||||
}
|
||||
| {
|
||||
data: {
|
||||
accessToken?: null | string;
|
||||
accessTokenExpiresAt?: null | number;
|
||||
accountId: string;
|
||||
createdAt: number;
|
||||
idToken?: null | string;
|
||||
password?: null | string;
|
||||
providerId: string;
|
||||
refreshToken?: null | string;
|
||||
refreshTokenExpiresAt?: null | number;
|
||||
scope?: null | string;
|
||||
updatedAt: number;
|
||||
userId: string;
|
||||
};
|
||||
model: "account";
|
||||
}
|
||||
| {
|
||||
data: {
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
identifier: string;
|
||||
updatedAt: number;
|
||||
value: string;
|
||||
};
|
||||
model: "verification";
|
||||
}
|
||||
| {
|
||||
data: {
|
||||
createdAt: number;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
};
|
||||
model: "jwks";
|
||||
};
|
||||
onCreateHandle?: string;
|
||||
select?: Array<string>;
|
||||
},
|
||||
any
|
||||
>;
|
||||
deleteMany: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{
|
||||
input:
|
||||
| {
|
||||
model: "user";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "name"
|
||||
| "email"
|
||||
| "emailVerified"
|
||||
| "image"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "session";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "expiresAt"
|
||||
| "token"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "ipAddress"
|
||||
| "userAgent"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "account";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "accountId"
|
||||
| "providerId"
|
||||
| "userId"
|
||||
| "accessToken"
|
||||
| "refreshToken"
|
||||
| "idToken"
|
||||
| "accessTokenExpiresAt"
|
||||
| "refreshTokenExpiresAt"
|
||||
| "scope"
|
||||
| "password"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "verification";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "identifier"
|
||||
| "value"
|
||||
| "expiresAt"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "jwks";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: "publicKey" | "privateKey" | "createdAt" | "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
};
|
||||
onDeleteHandle?: string;
|
||||
paginationOpts: {
|
||||
cursor: string | null;
|
||||
endCursor?: string | null;
|
||||
id?: number;
|
||||
maximumBytesRead?: number;
|
||||
maximumRowsRead?: number;
|
||||
numItems: number;
|
||||
};
|
||||
},
|
||||
any
|
||||
>;
|
||||
deleteOne: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{
|
||||
input:
|
||||
| {
|
||||
model: "user";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "name"
|
||||
| "email"
|
||||
| "emailVerified"
|
||||
| "image"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "session";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "expiresAt"
|
||||
| "token"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "ipAddress"
|
||||
| "userAgent"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "account";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "accountId"
|
||||
| "providerId"
|
||||
| "userId"
|
||||
| "accessToken"
|
||||
| "refreshToken"
|
||||
| "idToken"
|
||||
| "accessTokenExpiresAt"
|
||||
| "refreshTokenExpiresAt"
|
||||
| "scope"
|
||||
| "password"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "verification";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "identifier"
|
||||
| "value"
|
||||
| "expiresAt"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "jwks";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: "publicKey" | "privateKey" | "createdAt" | "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
};
|
||||
onDeleteHandle?: string;
|
||||
},
|
||||
any
|
||||
>;
|
||||
findMany: FunctionReference<
|
||||
"query",
|
||||
"internal",
|
||||
{
|
||||
limit?: number;
|
||||
model: "user" | "session" | "account" | "verification" | "jwks";
|
||||
offset?: number;
|
||||
paginationOpts: {
|
||||
cursor: string | null;
|
||||
endCursor?: string | null;
|
||||
id?: number;
|
||||
maximumBytesRead?: number;
|
||||
maximumRowsRead?: number;
|
||||
numItems: number;
|
||||
};
|
||||
sortBy?: { direction: "asc" | "desc"; field: string };
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: string;
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
},
|
||||
any
|
||||
>;
|
||||
findOne: FunctionReference<
|
||||
"query",
|
||||
"internal",
|
||||
{
|
||||
model: "user" | "session" | "account" | "verification" | "jwks";
|
||||
select?: Array<string>;
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: string;
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
},
|
||||
any
|
||||
>;
|
||||
updateMany: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{
|
||||
input:
|
||||
| {
|
||||
model: "user";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
email?: string;
|
||||
emailVerified?: boolean;
|
||||
image?: null | string;
|
||||
name?: string;
|
||||
updatedAt?: number;
|
||||
userId?: null | string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "name"
|
||||
| "email"
|
||||
| "emailVerified"
|
||||
| "image"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "session";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
expiresAt?: number;
|
||||
ipAddress?: null | string;
|
||||
token?: string;
|
||||
updatedAt?: number;
|
||||
userAgent?: null | string;
|
||||
userId?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "expiresAt"
|
||||
| "token"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "ipAddress"
|
||||
| "userAgent"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "account";
|
||||
update: {
|
||||
accessToken?: null | string;
|
||||
accessTokenExpiresAt?: null | number;
|
||||
accountId?: string;
|
||||
createdAt?: number;
|
||||
idToken?: null | string;
|
||||
password?: null | string;
|
||||
providerId?: string;
|
||||
refreshToken?: null | string;
|
||||
refreshTokenExpiresAt?: null | number;
|
||||
scope?: null | string;
|
||||
updatedAt?: number;
|
||||
userId?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "accountId"
|
||||
| "providerId"
|
||||
| "userId"
|
||||
| "accessToken"
|
||||
| "refreshToken"
|
||||
| "idToken"
|
||||
| "accessTokenExpiresAt"
|
||||
| "refreshTokenExpiresAt"
|
||||
| "scope"
|
||||
| "password"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "verification";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
expiresAt?: number;
|
||||
identifier?: string;
|
||||
updatedAt?: number;
|
||||
value?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "identifier"
|
||||
| "value"
|
||||
| "expiresAt"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "jwks";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
privateKey?: string;
|
||||
publicKey?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: "publicKey" | "privateKey" | "createdAt" | "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
};
|
||||
onUpdateHandle?: string;
|
||||
paginationOpts: {
|
||||
cursor: string | null;
|
||||
endCursor?: string | null;
|
||||
id?: number;
|
||||
maximumBytesRead?: number;
|
||||
maximumRowsRead?: number;
|
||||
numItems: number;
|
||||
};
|
||||
},
|
||||
any
|
||||
>;
|
||||
updateOne: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{
|
||||
input:
|
||||
| {
|
||||
model: "user";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
email?: string;
|
||||
emailVerified?: boolean;
|
||||
image?: null | string;
|
||||
name?: string;
|
||||
updatedAt?: number;
|
||||
userId?: null | string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "name"
|
||||
| "email"
|
||||
| "emailVerified"
|
||||
| "image"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "session";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
expiresAt?: number;
|
||||
ipAddress?: null | string;
|
||||
token?: string;
|
||||
updatedAt?: number;
|
||||
userAgent?: null | string;
|
||||
userId?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "expiresAt"
|
||||
| "token"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "ipAddress"
|
||||
| "userAgent"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "account";
|
||||
update: {
|
||||
accessToken?: null | string;
|
||||
accessTokenExpiresAt?: null | number;
|
||||
accountId?: string;
|
||||
createdAt?: number;
|
||||
idToken?: null | string;
|
||||
password?: null | string;
|
||||
providerId?: string;
|
||||
refreshToken?: null | string;
|
||||
refreshTokenExpiresAt?: null | number;
|
||||
scope?: null | string;
|
||||
updatedAt?: number;
|
||||
userId?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "accountId"
|
||||
| "providerId"
|
||||
| "userId"
|
||||
| "accessToken"
|
||||
| "refreshToken"
|
||||
| "idToken"
|
||||
| "accessTokenExpiresAt"
|
||||
| "refreshTokenExpiresAt"
|
||||
| "scope"
|
||||
| "password"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "verification";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
expiresAt?: number;
|
||||
identifier?: string;
|
||||
updatedAt?: number;
|
||||
value?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "identifier"
|
||||
| "value"
|
||||
| "expiresAt"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "jwks";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
privateKey?: string;
|
||||
publicKey?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: "publicKey" | "privateKey" | "createdAt" | "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
};
|
||||
onUpdateHandle?: string;
|
||||
},
|
||||
any
|
||||
>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { anyApi } from "convex/server";
|
||||
import { anyApi, componentsGeneric } from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
@@ -20,3 +20,4 @@ import { anyApi } from "convex/server";
|
||||
*/
|
||||
export const api = anyApi;
|
||||
export const internal = anyApi;
|
||||
export const components = componentsGeneric();
|
||||
|
||||
7
packages/convex/_generated/server.d.ts
vendored
7
packages/convex/_generated/server.d.ts
vendored
@@ -10,6 +10,7 @@
|
||||
|
||||
import {
|
||||
ActionBuilder,
|
||||
AnyComponents,
|
||||
HttpActionBuilder,
|
||||
MutationBuilder,
|
||||
QueryBuilder,
|
||||
@@ -18,9 +19,15 @@ import {
|
||||
GenericQueryCtx,
|
||||
GenericDatabaseReader,
|
||||
GenericDatabaseWriter,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
import type { DataModel } from "./dataModel.js";
|
||||
|
||||
type GenericCtx =
|
||||
| GenericActionCtx<DataModel>
|
||||
| GenericMutationCtx<DataModel>
|
||||
| GenericQueryCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
internalActionGeneric,
|
||||
internalMutationGeneric,
|
||||
internalQueryGeneric,
|
||||
componentsGeneric,
|
||||
} from "convex/server";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
const clientId = process.env.WORKOS_CLIENT_ID
|
||||
|
||||
const authConfig = {
|
||||
export default {
|
||||
providers: [
|
||||
{
|
||||
type: "customJwt",
|
||||
issuer: `https://api.workos.com/`,
|
||||
algorithm: "RS256",
|
||||
jwks: `https://api.workos.com/sso/jwks/${clientId}`,
|
||||
applicationID: clientId,
|
||||
},
|
||||
{
|
||||
type: "customJwt",
|
||||
issuer: `https://api.workos.com/user_management/${clientId}`,
|
||||
algorithm: "RS256",
|
||||
jwks: `https://api.workos.com/sso/jwks/${clientId}`,
|
||||
applicationID: clientId,
|
||||
domain: process.env.CONVEX_SITE_URL,
|
||||
applicationID: "convex",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default authConfig
|
||||
|
||||
58
packages/convex/auth.ts
Normal file
58
packages/convex/auth.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createClient, type GenericCtx } from "@convex-dev/better-auth"
|
||||
import { convex, crossDomain } from "@convex-dev/better-auth/plugins"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { components } from "./_generated/api"
|
||||
import type { DataModel } from "./_generated/dataModel"
|
||||
import authSchema from "./betterauth/schema"
|
||||
|
||||
const siteUrl = process.env.SITE_URL!
|
||||
|
||||
// The component client has methods needed for integrating Convex with Better Auth,
|
||||
// as well as helper methods for general use.
|
||||
export const authComponent = createClient<DataModel, typeof authSchema>(
|
||||
components.betterAuth,
|
||||
{
|
||||
local: {
|
||||
schema: authSchema,
|
||||
},
|
||||
triggers: {
|
||||
user: {
|
||||
onCreate: async (ctx, user) => {
|
||||
const now = Date.now()
|
||||
await ctx.db.insert("directories", {
|
||||
name: "",
|
||||
userId: user._id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export const createAuth = (
|
||||
ctx: GenericCtx<DataModel>,
|
||||
{ optionsOnly } = { optionsOnly: false },
|
||||
) => {
|
||||
return betterAuth({
|
||||
// disable logging when createAuth is called just to generate options.
|
||||
// this is not required, but there's a lot of noise in logs without it.
|
||||
logger: {
|
||||
disabled: optionsOnly,
|
||||
},
|
||||
trustedOrigins: [siteUrl],
|
||||
database: authComponent.adapter(ctx),
|
||||
// Configure simple, non-verified email/password to get started
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: false,
|
||||
},
|
||||
plugins: [
|
||||
// The cross domain plugin is required for client side frameworks
|
||||
crossDomain({ siteUrl }),
|
||||
// The Convex plugin is required for Convex compatibility
|
||||
convex(),
|
||||
],
|
||||
})
|
||||
}
|
||||
971
packages/convex/betterauth/_generated/api.d.ts
vendored
Normal file
971
packages/convex/betterauth/_generated/api.d.ts
vendored
Normal file
@@ -0,0 +1,971 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type * as adapter from "../adapter.js";
|
||||
import type * as auth from "../auth.js";
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
FilterApi,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
declare const fullApi: ApiFromModules<{
|
||||
adapter: typeof adapter;
|
||||
auth: typeof auth;
|
||||
}>;
|
||||
export type Mounts = {
|
||||
adapter: {
|
||||
create: FunctionReference<
|
||||
"mutation",
|
||||
"public",
|
||||
{
|
||||
input:
|
||||
| {
|
||||
data: {
|
||||
createdAt: number;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
image?: null | string;
|
||||
name: string;
|
||||
updatedAt: number;
|
||||
userId?: null | string;
|
||||
};
|
||||
model: "user";
|
||||
}
|
||||
| {
|
||||
data: {
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
ipAddress?: null | string;
|
||||
token: string;
|
||||
updatedAt: number;
|
||||
userAgent?: null | string;
|
||||
userId: string;
|
||||
};
|
||||
model: "session";
|
||||
}
|
||||
| {
|
||||
data: {
|
||||
accessToken?: null | string;
|
||||
accessTokenExpiresAt?: null | number;
|
||||
accountId: string;
|
||||
createdAt: number;
|
||||
idToken?: null | string;
|
||||
password?: null | string;
|
||||
providerId: string;
|
||||
refreshToken?: null | string;
|
||||
refreshTokenExpiresAt?: null | number;
|
||||
scope?: null | string;
|
||||
updatedAt: number;
|
||||
userId: string;
|
||||
};
|
||||
model: "account";
|
||||
}
|
||||
| {
|
||||
data: {
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
identifier: string;
|
||||
updatedAt: number;
|
||||
value: string;
|
||||
};
|
||||
model: "verification";
|
||||
}
|
||||
| {
|
||||
data: {
|
||||
createdAt: number;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
};
|
||||
model: "jwks";
|
||||
};
|
||||
onCreateHandle?: string;
|
||||
select?: Array<string>;
|
||||
},
|
||||
any
|
||||
>;
|
||||
deleteMany: FunctionReference<
|
||||
"mutation",
|
||||
"public",
|
||||
{
|
||||
input:
|
||||
| {
|
||||
model: "user";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "name"
|
||||
| "email"
|
||||
| "emailVerified"
|
||||
| "image"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "session";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "expiresAt"
|
||||
| "token"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "ipAddress"
|
||||
| "userAgent"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "account";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "accountId"
|
||||
| "providerId"
|
||||
| "userId"
|
||||
| "accessToken"
|
||||
| "refreshToken"
|
||||
| "idToken"
|
||||
| "accessTokenExpiresAt"
|
||||
| "refreshTokenExpiresAt"
|
||||
| "scope"
|
||||
| "password"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "verification";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "identifier"
|
||||
| "value"
|
||||
| "expiresAt"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "jwks";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: "publicKey" | "privateKey" | "createdAt" | "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
};
|
||||
onDeleteHandle?: string;
|
||||
paginationOpts: {
|
||||
cursor: string | null;
|
||||
endCursor?: string | null;
|
||||
id?: number;
|
||||
maximumBytesRead?: number;
|
||||
maximumRowsRead?: number;
|
||||
numItems: number;
|
||||
};
|
||||
},
|
||||
any
|
||||
>;
|
||||
deleteOne: FunctionReference<
|
||||
"mutation",
|
||||
"public",
|
||||
{
|
||||
input:
|
||||
| {
|
||||
model: "user";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "name"
|
||||
| "email"
|
||||
| "emailVerified"
|
||||
| "image"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "session";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "expiresAt"
|
||||
| "token"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "ipAddress"
|
||||
| "userAgent"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "account";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "accountId"
|
||||
| "providerId"
|
||||
| "userId"
|
||||
| "accessToken"
|
||||
| "refreshToken"
|
||||
| "idToken"
|
||||
| "accessTokenExpiresAt"
|
||||
| "refreshTokenExpiresAt"
|
||||
| "scope"
|
||||
| "password"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "verification";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "identifier"
|
||||
| "value"
|
||||
| "expiresAt"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "jwks";
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: "publicKey" | "privateKey" | "createdAt" | "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
};
|
||||
onDeleteHandle?: string;
|
||||
},
|
||||
any
|
||||
>;
|
||||
findMany: FunctionReference<
|
||||
"query",
|
||||
"public",
|
||||
{
|
||||
limit?: number;
|
||||
model: "user" | "session" | "account" | "verification" | "jwks";
|
||||
offset?: number;
|
||||
paginationOpts: {
|
||||
cursor: string | null;
|
||||
endCursor?: string | null;
|
||||
id?: number;
|
||||
maximumBytesRead?: number;
|
||||
maximumRowsRead?: number;
|
||||
numItems: number;
|
||||
};
|
||||
sortBy?: { direction: "asc" | "desc"; field: string };
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: string;
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
},
|
||||
any
|
||||
>;
|
||||
findOne: FunctionReference<
|
||||
"query",
|
||||
"public",
|
||||
{
|
||||
model: "user" | "session" | "account" | "verification" | "jwks";
|
||||
select?: Array<string>;
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: string;
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
},
|
||||
any
|
||||
>;
|
||||
updateMany: FunctionReference<
|
||||
"mutation",
|
||||
"public",
|
||||
{
|
||||
input:
|
||||
| {
|
||||
model: "user";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
email?: string;
|
||||
emailVerified?: boolean;
|
||||
image?: null | string;
|
||||
name?: string;
|
||||
updatedAt?: number;
|
||||
userId?: null | string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "name"
|
||||
| "email"
|
||||
| "emailVerified"
|
||||
| "image"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "session";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
expiresAt?: number;
|
||||
ipAddress?: null | string;
|
||||
token?: string;
|
||||
updatedAt?: number;
|
||||
userAgent?: null | string;
|
||||
userId?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "expiresAt"
|
||||
| "token"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "ipAddress"
|
||||
| "userAgent"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "account";
|
||||
update: {
|
||||
accessToken?: null | string;
|
||||
accessTokenExpiresAt?: null | number;
|
||||
accountId?: string;
|
||||
createdAt?: number;
|
||||
idToken?: null | string;
|
||||
password?: null | string;
|
||||
providerId?: string;
|
||||
refreshToken?: null | string;
|
||||
refreshTokenExpiresAt?: null | number;
|
||||
scope?: null | string;
|
||||
updatedAt?: number;
|
||||
userId?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "accountId"
|
||||
| "providerId"
|
||||
| "userId"
|
||||
| "accessToken"
|
||||
| "refreshToken"
|
||||
| "idToken"
|
||||
| "accessTokenExpiresAt"
|
||||
| "refreshTokenExpiresAt"
|
||||
| "scope"
|
||||
| "password"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "verification";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
expiresAt?: number;
|
||||
identifier?: string;
|
||||
updatedAt?: number;
|
||||
value?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "identifier"
|
||||
| "value"
|
||||
| "expiresAt"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "jwks";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
privateKey?: string;
|
||||
publicKey?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: "publicKey" | "privateKey" | "createdAt" | "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
};
|
||||
onUpdateHandle?: string;
|
||||
paginationOpts: {
|
||||
cursor: string | null;
|
||||
endCursor?: string | null;
|
||||
id?: number;
|
||||
maximumBytesRead?: number;
|
||||
maximumRowsRead?: number;
|
||||
numItems: number;
|
||||
};
|
||||
},
|
||||
any
|
||||
>;
|
||||
updateOne: FunctionReference<
|
||||
"mutation",
|
||||
"public",
|
||||
{
|
||||
input:
|
||||
| {
|
||||
model: "user";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
email?: string;
|
||||
emailVerified?: boolean;
|
||||
image?: null | string;
|
||||
name?: string;
|
||||
updatedAt?: number;
|
||||
userId?: null | string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "name"
|
||||
| "email"
|
||||
| "emailVerified"
|
||||
| "image"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "session";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
expiresAt?: number;
|
||||
ipAddress?: null | string;
|
||||
token?: string;
|
||||
updatedAt?: number;
|
||||
userAgent?: null | string;
|
||||
userId?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "expiresAt"
|
||||
| "token"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "ipAddress"
|
||||
| "userAgent"
|
||||
| "userId"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "account";
|
||||
update: {
|
||||
accessToken?: null | string;
|
||||
accessTokenExpiresAt?: null | number;
|
||||
accountId?: string;
|
||||
createdAt?: number;
|
||||
idToken?: null | string;
|
||||
password?: null | string;
|
||||
providerId?: string;
|
||||
refreshToken?: null | string;
|
||||
refreshTokenExpiresAt?: null | number;
|
||||
scope?: null | string;
|
||||
updatedAt?: number;
|
||||
userId?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "accountId"
|
||||
| "providerId"
|
||||
| "userId"
|
||||
| "accessToken"
|
||||
| "refreshToken"
|
||||
| "idToken"
|
||||
| "accessTokenExpiresAt"
|
||||
| "refreshTokenExpiresAt"
|
||||
| "scope"
|
||||
| "password"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "verification";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
expiresAt?: number;
|
||||
identifier?: string;
|
||||
updatedAt?: number;
|
||||
value?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field:
|
||||
| "identifier"
|
||||
| "value"
|
||||
| "expiresAt"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
model: "jwks";
|
||||
update: {
|
||||
createdAt?: number;
|
||||
privateKey?: string;
|
||||
publicKey?: string;
|
||||
};
|
||||
where?: Array<{
|
||||
connector?: "AND" | "OR";
|
||||
field: "publicKey" | "privateKey" | "createdAt" | "id";
|
||||
operator?:
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "eq"
|
||||
| "in"
|
||||
| "ne"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with";
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string>
|
||||
| Array<number>
|
||||
| null;
|
||||
}>;
|
||||
};
|
||||
onUpdateHandle?: string;
|
||||
},
|
||||
any
|
||||
>;
|
||||
};
|
||||
};
|
||||
// For now fullApiWithMounts is only fullApi which provides
|
||||
// jump-to-definition in component client code.
|
||||
// Use Mounts for the same type without the inference.
|
||||
declare const fullApiWithMounts: typeof fullApi;
|
||||
|
||||
export declare const api: FilterApi<
|
||||
typeof fullApiWithMounts,
|
||||
FunctionReference<any, "public">
|
||||
>;
|
||||
export declare const internal: FilterApi<
|
||||
typeof fullApiWithMounts,
|
||||
FunctionReference<any, "internal">
|
||||
>;
|
||||
|
||||
export declare const components: {};
|
||||
23
packages/convex/betterauth/_generated/api.js
Normal file
23
packages/convex/betterauth/_generated/api.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { anyApi, componentsGeneric } from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export const api = anyApi;
|
||||
export const internal = anyApi;
|
||||
export const components = componentsGeneric();
|
||||
60
packages/convex/betterauth/_generated/dataModel.d.ts
vendored
Normal file
60
packages/convex/betterauth/_generated/dataModel.d.ts
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated data model types.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
DataModelFromSchemaDefinition,
|
||||
DocumentByName,
|
||||
TableNamesInDataModel,
|
||||
SystemTableNames,
|
||||
} from "convex/server";
|
||||
import type { GenericId } from "convex/values";
|
||||
import schema from "../schema.js";
|
||||
|
||||
/**
|
||||
* The names of all of your Convex tables.
|
||||
*/
|
||||
export type TableNames = TableNamesInDataModel<DataModel>;
|
||||
|
||||
/**
|
||||
* The type of a document stored in Convex.
|
||||
*
|
||||
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||
*/
|
||||
export type Doc<TableName extends TableNames> = DocumentByName<
|
||||
DataModel,
|
||||
TableName
|
||||
>;
|
||||
|
||||
/**
|
||||
* An identifier for a document in Convex.
|
||||
*
|
||||
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||
*
|
||||
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
||||
*
|
||||
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||
* strings when type checking.
|
||||
*
|
||||
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||
*/
|
||||
export type Id<TableName extends TableNames | SystemTableNames> =
|
||||
GenericId<TableName>;
|
||||
|
||||
/**
|
||||
* A type describing your Convex data model.
|
||||
*
|
||||
* This type includes information about what tables you have, the type of
|
||||
* documents stored in those tables, and the indexes defined on them.
|
||||
*
|
||||
* This type is used to parameterize methods like `queryGeneric` and
|
||||
* `mutationGeneric` to make them type-safe.
|
||||
*/
|
||||
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
||||
149
packages/convex/betterauth/_generated/server.d.ts
vendored
Normal file
149
packages/convex/betterauth/_generated/server.d.ts
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionBuilder,
|
||||
AnyComponents,
|
||||
HttpActionBuilder,
|
||||
MutationBuilder,
|
||||
QueryBuilder,
|
||||
GenericActionCtx,
|
||||
GenericMutationCtx,
|
||||
GenericQueryCtx,
|
||||
GenericDatabaseReader,
|
||||
GenericDatabaseWriter,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
import type { DataModel } from "./dataModel.js";
|
||||
|
||||
type GenericCtx =
|
||||
| GenericActionCtx<DataModel>
|
||||
| GenericMutationCtx<DataModel>
|
||||
| GenericQueryCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const query: QueryBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const mutation: MutationBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const action: ActionBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define an HTTP action.
|
||||
*
|
||||
* This function will be used to respond to HTTP requests received by a Convex
|
||||
* deployment if the requests matches the path and method where this action
|
||||
* is routed. Be sure to route your action in `convex/http.js`.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||
*/
|
||||
export declare const httpAction: HttpActionBuilder;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex query functions.
|
||||
*
|
||||
* The query context is passed as the first argument to any Convex query
|
||||
* function run on the server.
|
||||
*
|
||||
* This differs from the {@link MutationCtx} because all of the services are
|
||||
* read-only.
|
||||
*/
|
||||
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex mutation functions.
|
||||
*
|
||||
* The mutation context is passed as the first argument to any Convex mutation
|
||||
* function run on the server.
|
||||
*/
|
||||
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex action functions.
|
||||
*
|
||||
* The action context is passed as the first argument to any Convex action
|
||||
* function run on the server.
|
||||
*/
|
||||
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from the database within Convex query functions.
|
||||
*
|
||||
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
||||
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||
* building a query.
|
||||
*/
|
||||
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from and write to the database within Convex mutation
|
||||
* functions.
|
||||
*
|
||||
* Convex guarantees that all writes within a single mutation are
|
||||
* executed atomically, so you never have to worry about partial writes leaving
|
||||
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
||||
* for the guarantees Convex provides your functions.
|
||||
*/
|
||||
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
||||
90
packages/convex/betterauth/_generated/server.js
Normal file
90
packages/convex/betterauth/_generated/server.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
actionGeneric,
|
||||
httpActionGeneric,
|
||||
queryGeneric,
|
||||
mutationGeneric,
|
||||
internalActionGeneric,
|
||||
internalMutationGeneric,
|
||||
internalQueryGeneric,
|
||||
componentsGeneric,
|
||||
} from "convex/server";
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const query = queryGeneric;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalQuery = internalQueryGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const mutation = mutationGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalMutation = internalMutationGeneric;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const action = actionGeneric;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalAction = internalActionGeneric;
|
||||
|
||||
/**
|
||||
* Define a Convex HTTP action.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
|
||||
* as its second.
|
||||
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
|
||||
*/
|
||||
export const httpAction = httpActionGeneric;
|
||||
13
packages/convex/betterauth/adapter.ts
Normal file
13
packages/convex/betterauth/adapter.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createApi } from "@convex-dev/better-auth";
|
||||
import schema from "./schema";
|
||||
import { createAuth } from "../auth";
|
||||
|
||||
export const {
|
||||
create,
|
||||
findOne,
|
||||
findMany,
|
||||
updateOne,
|
||||
updateMany,
|
||||
deleteOne,
|
||||
deleteMany,
|
||||
} = createApi(schema, createAuth);
|
||||
5
packages/convex/betterauth/auth.ts
Normal file
5
packages/convex/betterauth/auth.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createAuth } from '../auth'
|
||||
import { getStaticAuth } from '@convex-dev/better-auth'
|
||||
|
||||
// Export a static instance for Better Auth schema generation
|
||||
export const auth = getStaticAuth(createAuth)
|
||||
5
packages/convex/betterauth/convex.config.ts
Normal file
5
packages/convex/betterauth/convex.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { defineComponent } from "convex/server";
|
||||
|
||||
const component = defineComponent("betterAuth");
|
||||
|
||||
export default component;
|
||||
70
packages/convex/betterauth/schema.ts
Normal file
70
packages/convex/betterauth/schema.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// This file is auto-generated. Do not edit this file manually.
|
||||
// To regenerate the schema, run:
|
||||
// `npx @better-auth/cli generate --output undefined -y`
|
||||
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const tables = {
|
||||
user: defineTable({
|
||||
name: v.string(),
|
||||
email: v.string(),
|
||||
emailVerified: v.boolean(),
|
||||
image: v.optional(v.union(v.null(), v.string())),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
userId: v.optional(v.union(v.null(), v.string())),
|
||||
})
|
||||
.index("email_name", ["email","name"])
|
||||
.index("name", ["name"])
|
||||
.index("userId", ["userId"]),
|
||||
session: defineTable({
|
||||
expiresAt: v.number(),
|
||||
token: v.string(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
ipAddress: v.optional(v.union(v.null(), v.string())),
|
||||
userAgent: v.optional(v.union(v.null(), v.string())),
|
||||
userId: v.string(),
|
||||
})
|
||||
.index("expiresAt", ["expiresAt"])
|
||||
.index("expiresAt_userId", ["expiresAt","userId"])
|
||||
.index("token", ["token"])
|
||||
.index("userId", ["userId"]),
|
||||
account: defineTable({
|
||||
accountId: v.string(),
|
||||
providerId: v.string(),
|
||||
userId: v.string(),
|
||||
accessToken: v.optional(v.union(v.null(), v.string())),
|
||||
refreshToken: v.optional(v.union(v.null(), v.string())),
|
||||
idToken: v.optional(v.union(v.null(), v.string())),
|
||||
accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
|
||||
refreshTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
|
||||
scope: v.optional(v.union(v.null(), v.string())),
|
||||
password: v.optional(v.union(v.null(), v.string())),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("accountId", ["accountId"])
|
||||
.index("accountId_providerId", ["accountId","providerId"])
|
||||
.index("providerId_userId", ["providerId","userId"])
|
||||
.index("userId", ["userId"]),
|
||||
verification: defineTable({
|
||||
identifier: v.string(),
|
||||
value: v.string(),
|
||||
expiresAt: v.number(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("expiresAt", ["expiresAt"])
|
||||
.index("identifier", ["identifier"]),
|
||||
jwks: defineTable({
|
||||
publicKey: v.string(),
|
||||
privateKey: v.string(),
|
||||
createdAt: v.number(),
|
||||
}),
|
||||
};
|
||||
|
||||
const schema = defineSchema(tables);
|
||||
|
||||
export default schema;
|
||||
7
packages/convex/convex.config.ts
Normal file
7
packages/convex/convex.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineApp } from "convex/server"
|
||||
import betterAuth from "./betterauth/convex.config"
|
||||
|
||||
const app = defineApp()
|
||||
app.use(betterAuth)
|
||||
|
||||
export default app
|
||||
33
packages/convex/convex/_generated/api.d.ts
vendored
Normal file
33
packages/convex/convex/_generated/api.d.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
FilterApi,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
declare const fullApi: ApiFromModules<{}>;
|
||||
export declare const api: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "public">
|
||||
>;
|
||||
export declare const internal: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "internal">
|
||||
>;
|
||||
22
packages/convex/convex/_generated/api.js
Normal file
22
packages/convex/convex/_generated/api.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { anyApi } from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export const api = anyApi;
|
||||
export const internal = anyApi;
|
||||
58
packages/convex/convex/_generated/dataModel.d.ts
vendored
Normal file
58
packages/convex/convex/_generated/dataModel.d.ts
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated data model types.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { AnyDataModel } from "convex/server";
|
||||
import type { GenericId } from "convex/values";
|
||||
|
||||
/**
|
||||
* No `schema.ts` file found!
|
||||
*
|
||||
* This generated code has permissive types like `Doc = any` because
|
||||
* Convex doesn't know your schema. If you'd like more type safety, see
|
||||
* https://docs.convex.dev/using/schemas for instructions on how to add a
|
||||
* schema file.
|
||||
*
|
||||
* After you change a schema, rerun codegen with `npx convex dev`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The names of all of your Convex tables.
|
||||
*/
|
||||
export type TableNames = string;
|
||||
|
||||
/**
|
||||
* The type of a document stored in Convex.
|
||||
*/
|
||||
export type Doc = any;
|
||||
|
||||
/**
|
||||
* An identifier for a document in Convex.
|
||||
*
|
||||
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||
*
|
||||
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
||||
*
|
||||
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||
* strings when type checking.
|
||||
*/
|
||||
export type Id<TableName extends TableNames = TableNames> =
|
||||
GenericId<TableName>;
|
||||
|
||||
/**
|
||||
* A type describing your Convex data model.
|
||||
*
|
||||
* This type includes information about what tables you have, the type of
|
||||
* documents stored in those tables, and the indexes defined on them.
|
||||
*
|
||||
* This type is used to parameterize methods like `queryGeneric` and
|
||||
* `mutationGeneric` to make them type-safe.
|
||||
*/
|
||||
export type DataModel = AnyDataModel;
|
||||
142
packages/convex/convex/_generated/server.d.ts
vendored
Normal file
142
packages/convex/convex/_generated/server.d.ts
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionBuilder,
|
||||
HttpActionBuilder,
|
||||
MutationBuilder,
|
||||
QueryBuilder,
|
||||
GenericActionCtx,
|
||||
GenericMutationCtx,
|
||||
GenericQueryCtx,
|
||||
GenericDatabaseReader,
|
||||
GenericDatabaseWriter,
|
||||
} from "convex/server";
|
||||
import type { DataModel } from "./dataModel.js";
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const query: QueryBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const mutation: MutationBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const action: ActionBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define an HTTP action.
|
||||
*
|
||||
* This function will be used to respond to HTTP requests received by a Convex
|
||||
* deployment if the requests matches the path and method where this action
|
||||
* is routed. Be sure to route your action in `convex/http.js`.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||
*/
|
||||
export declare const httpAction: HttpActionBuilder;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex query functions.
|
||||
*
|
||||
* The query context is passed as the first argument to any Convex query
|
||||
* function run on the server.
|
||||
*
|
||||
* This differs from the {@link MutationCtx} because all of the services are
|
||||
* read-only.
|
||||
*/
|
||||
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex mutation functions.
|
||||
*
|
||||
* The mutation context is passed as the first argument to any Convex mutation
|
||||
* function run on the server.
|
||||
*/
|
||||
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex action functions.
|
||||
*
|
||||
* The action context is passed as the first argument to any Convex action
|
||||
* function run on the server.
|
||||
*/
|
||||
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from the database within Convex query functions.
|
||||
*
|
||||
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
||||
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||
* building a query.
|
||||
*/
|
||||
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from and write to the database within Convex mutation
|
||||
* functions.
|
||||
*
|
||||
* Convex guarantees that all writes within a single mutation are
|
||||
* executed atomically, so you never have to worry about partial writes leaving
|
||||
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
||||
* for the guarantees Convex provides your functions.
|
||||
*/
|
||||
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
||||
89
packages/convex/convex/_generated/server.js
Normal file
89
packages/convex/convex/_generated/server.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
actionGeneric,
|
||||
httpActionGeneric,
|
||||
queryGeneric,
|
||||
mutationGeneric,
|
||||
internalActionGeneric,
|
||||
internalMutationGeneric,
|
||||
internalQueryGeneric,
|
||||
} from "convex/server";
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const query = queryGeneric;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalQuery = internalQueryGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const mutation = mutationGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalMutation = internalMutationGeneric;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const action = actionGeneric;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalAction = internalActionGeneric;
|
||||
|
||||
/**
|
||||
* Define a Convex HTTP action.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
|
||||
* as its second.
|
||||
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
|
||||
*/
|
||||
export const httpAction = httpActionGeneric;
|
||||
@@ -1,26 +1,16 @@
|
||||
import { v } from "convex/values"
|
||||
import type { Id } from "./_generated/dataModel"
|
||||
import { authenticatedMutation, authenticatedQuery } from "./functions"
|
||||
import { authenticatedMutation, authenticatedQuery, authorizedGet } from "./functions"
|
||||
import * as Directories from "./model/directories"
|
||||
import * as Files from "./model/files"
|
||||
import type { FileSystemItem } from "./model/filesystem"
|
||||
|
||||
export const generateUploadUrl = authenticatedMutation({
|
||||
handler: async (ctx) => {
|
||||
// ctx.user and ctx.identity are automatically available
|
||||
return await ctx.storage.generateUploadUrl()
|
||||
},
|
||||
})
|
||||
|
||||
export const generateFileUrl = authenticatedQuery({
|
||||
args: {
|
||||
storageId: v.id("_storage"),
|
||||
},
|
||||
handler: async (ctx, { storageId }) => {
|
||||
return await ctx.storage.getUrl(storageId)
|
||||
},
|
||||
})
|
||||
|
||||
export const fetchFiles = authenticatedQuery({
|
||||
args: {
|
||||
directoryId: v.optional(v.id("directories")),
|
||||
@@ -46,25 +36,25 @@ export const fetchDirectory = authenticatedQuery({
|
||||
directoryId: v.id("directories"),
|
||||
},
|
||||
handler: async (ctx, { directoryId }) => {
|
||||
const directory = await authorizedGet(ctx, directoryId)
|
||||
if (!directory) {
|
||||
throw new Error("Directory not found")
|
||||
}
|
||||
return await Directories.fetch(ctx, { directoryId })
|
||||
},
|
||||
})
|
||||
|
||||
export const fetchDirectoryContent = authenticatedQuery({
|
||||
args: {
|
||||
directoryId: v.optional(v.id("directories")),
|
||||
},
|
||||
handler: async (ctx, { directoryId }): Promise<FileSystemItem[]> => {
|
||||
return await Directories.fetchContent(ctx, { directoryId })
|
||||
},
|
||||
})
|
||||
|
||||
export const createDirectory = authenticatedMutation({
|
||||
args: {
|
||||
name: v.string(),
|
||||
directoryId: v.id("directories"),
|
||||
},
|
||||
handler: async (ctx, { name, directoryId }): Promise<Id<"directories">> => {
|
||||
const parentDirectory = await authorizedGet(ctx, directoryId)
|
||||
if (!parentDirectory) {
|
||||
throw new Error("Parent directory not found")
|
||||
}
|
||||
|
||||
return await Directories.create(ctx, {
|
||||
name,
|
||||
parentId: directoryId,
|
||||
@@ -81,7 +71,12 @@ export const saveFile = authenticatedMutation({
|
||||
mimeType: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, { name, storageId, directoryId, size, mimeType }) => {
|
||||
const now = new Date().toISOString()
|
||||
const directory = await authorizedGet(ctx, directoryId)
|
||||
if (!directory) {
|
||||
throw new Error("Directory not found")
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
await ctx.db.insert("files", {
|
||||
name,
|
||||
@@ -103,6 +98,11 @@ export const renameFile = authenticatedMutation({
|
||||
newName: v.string(),
|
||||
},
|
||||
handler: async (ctx, { directoryId, itemId, newName }) => {
|
||||
const file = await authorizedGet(ctx, itemId)
|
||||
if (!file) {
|
||||
throw new Error("File not found")
|
||||
}
|
||||
|
||||
await Files.renameFile(ctx, { directoryId, itemId, newName })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { v } from "convex/values"
|
||||
import { authenticatedMutation } from "./functions"
|
||||
import { authenticatedMutation, authenticatedQuery, authorizedGet } from "./functions"
|
||||
import * as Directories from "./model/directories"
|
||||
import * as Err from "./model/error"
|
||||
import * as Files from "./model/files"
|
||||
import type { DirectoryHandle, FileHandle } from "./model/filesystem"
|
||||
import type {
|
||||
DirectoryHandle,
|
||||
FileHandle,
|
||||
FileSystemItem,
|
||||
} from "./model/filesystem"
|
||||
import * as FileSystem from "./model/filesystem"
|
||||
import {
|
||||
type FileSystemHandle,
|
||||
FileType,
|
||||
@@ -17,10 +22,7 @@ export const moveItems = authenticatedMutation({
|
||||
items: v.array(VFileSystemHandle),
|
||||
},
|
||||
handler: async (ctx, { targetDirectory: targetDirectoryHandle, items }) => {
|
||||
const targetDirectory = await Directories.fetchHandle(
|
||||
ctx,
|
||||
targetDirectoryHandle,
|
||||
)
|
||||
const targetDirectory = await authorizedGet(ctx, targetDirectoryHandle.id)
|
||||
if (!targetDirectory) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
@@ -64,13 +66,23 @@ export const moveToTrash = authenticatedMutation({
|
||||
handles: v.array(VFileSystemHandle),
|
||||
},
|
||||
handler: async (ctx, { handles }) => {
|
||||
for (const handle of handles) {
|
||||
const item = await authorizedGet(ctx, handle.id)
|
||||
if (!item) {
|
||||
throw Err.create(
|
||||
Err.Code.NotFound,
|
||||
`Item ${handle.id} not found`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/useIterableCallbackReturn: switch statement is exhaustive
|
||||
const promises = handles.map((handle) => {
|
||||
switch (handle.kind) {
|
||||
case FileType.File:
|
||||
return ctx.db
|
||||
.patch(handle.id, {
|
||||
deletedAt: new Date().toISOString(),
|
||||
deletedAt: Date.now(),
|
||||
})
|
||||
.then(() => handle)
|
||||
case FileType.Directory:
|
||||
@@ -100,3 +112,54 @@ export const moveToTrash = authenticatedMutation({
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const fetchDirectoryContent = authenticatedQuery({
|
||||
args: {
|
||||
directoryId: v.optional(v.id("directories")),
|
||||
trashed: v.boolean(),
|
||||
},
|
||||
handler: async (
|
||||
ctx,
|
||||
{ directoryId, trashed },
|
||||
): Promise<FileSystemItem[]> => {
|
||||
return await Directories.fetchContent(ctx, { directoryId, trashed })
|
||||
},
|
||||
})
|
||||
|
||||
export const permanentlyDeleteItems = authenticatedMutation({
|
||||
args: {
|
||||
handles: v.array(VFileSystemHandle),
|
||||
},
|
||||
handler: async (ctx, { handles }) => {
|
||||
return await FileSystem.deleteItemsPermanently(ctx, { handles })
|
||||
},
|
||||
})
|
||||
|
||||
export const emptyTrash = authenticatedMutation({
|
||||
handler: async (ctx) => {
|
||||
return await FileSystem.emptyTrash(ctx)
|
||||
},
|
||||
})
|
||||
|
||||
export const restoreItems = authenticatedMutation({
|
||||
args: {
|
||||
handles: v.array(VFileSystemHandle),
|
||||
},
|
||||
handler: async (ctx, { handles }) => {
|
||||
return await FileSystem.restoreItems(ctx, { handles })
|
||||
},
|
||||
})
|
||||
|
||||
export const fetchFileUrl = authenticatedQuery({
|
||||
args: {
|
||||
fileId: v.id("files"),
|
||||
},
|
||||
handler: async (ctx, { fileId }) => {
|
||||
const file = await authorizedGet(ctx, fileId)
|
||||
if (!file) {
|
||||
throw Err.create(Err.Code.NotFound, "File not found")
|
||||
}
|
||||
|
||||
return await FileSystem.fetchFileUrl(ctx, { fileId })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import type { UserIdentity } from "convex/server"
|
||||
import type {
|
||||
DocumentByName,
|
||||
TableNamesInDataModel,
|
||||
UserIdentity,
|
||||
} from "convex/server"
|
||||
import type { GenericId } from "convex/values"
|
||||
import {
|
||||
customCtx,
|
||||
customMutation,
|
||||
customQuery,
|
||||
} from "convex-helpers/server/customFunctions"
|
||||
import type { Doc } from "./_generated/dataModel"
|
||||
import type { DataModel } from "./_generated/dataModel"
|
||||
import type { MutationCtx, QueryCtx } from "./_generated/server"
|
||||
import { mutation, query } from "./_generated/server"
|
||||
import { userIdentityOrThrow, userOrThrow } from "./model/user"
|
||||
import { type AuthUser, userIdentityOrThrow, userOrThrow } from "./model/user"
|
||||
|
||||
export type AuthenticatedQueryCtx = QueryCtx & {
|
||||
user: Doc<"users">
|
||||
user: AuthUser
|
||||
identity: UserIdentity
|
||||
}
|
||||
|
||||
export type AuthenticatedMutationCtx = MutationCtx & {
|
||||
user: Doc<"users">
|
||||
user: AuthUser
|
||||
identity: UserIdentity
|
||||
}
|
||||
|
||||
@@ -44,3 +49,19 @@ export const authenticatedMutation = customMutation(
|
||||
return { user, identity }
|
||||
}),
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets a document by its id and checks if the user is authorized to access it
|
||||
*
|
||||
* @returns The document associated with the id or null if the document is not found.
|
||||
*/
|
||||
export async function authorizedGet<T extends TableNamesInDataModel<DataModel>>(
|
||||
ctx: AuthenticatedQueryCtx | AuthenticatedMutationCtx,
|
||||
id: GenericId<T>,
|
||||
): Promise<DocumentByName<DataModel, T> | null> {
|
||||
const item = await ctx.db.get(id)
|
||||
if (item && item.userId !== ctx.user._id) {
|
||||
return null
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
7
packages/convex/http.ts
Normal file
7
packages/convex/http.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { httpRouter } from "convex/server"
|
||||
import { authComponent, createAuth } from "./auth"
|
||||
|
||||
const http = httpRouter()
|
||||
// CORS handling is required for client side frameworks
|
||||
authComponent.registerRoutes(http, createAuth, { cors: true })
|
||||
export default http
|
||||
@@ -3,17 +3,17 @@ import type {
|
||||
AuthenticatedMutationCtx,
|
||||
AuthenticatedQueryCtx,
|
||||
} from "../functions"
|
||||
import { authorizedGet } from "../functions"
|
||||
import * as Err from "./error"
|
||||
import {
|
||||
type DirectoryHandle,
|
||||
type FilePath,
|
||||
type DirectoryPath,
|
||||
type FileSystemItem,
|
||||
FileType,
|
||||
newDirectoryHandle,
|
||||
type ReverseFilePath,
|
||||
} from "./filesystem"
|
||||
|
||||
export type DirectoryInfo = Doc<"directories"> & { path: FilePath }
|
||||
export type DirectoryInfo = Doc<"directories"> & { path: DirectoryPath }
|
||||
|
||||
export async function fetchRoot(ctx: AuthenticatedQueryCtx) {
|
||||
return await ctx.db
|
||||
@@ -28,8 +28,8 @@ export async function fetchHandle(
|
||||
ctx: AuthenticatedQueryCtx,
|
||||
handle: DirectoryHandle,
|
||||
): Promise<Doc<"directories">> {
|
||||
const directory = await ctx.db.get(handle.id)
|
||||
if (!directory || directory.userId !== ctx.user._id) {
|
||||
const directory = await authorizedGet(ctx, handle.id)
|
||||
if (!directory) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
`Directory ${handle.id} not found`,
|
||||
@@ -42,7 +42,7 @@ export async function fetch(
|
||||
ctx: AuthenticatedQueryCtx,
|
||||
{ directoryId }: { directoryId: Id<"directories"> },
|
||||
): Promise<DirectoryInfo> {
|
||||
const directory = await ctx.db.get(directoryId)
|
||||
const directory = await authorizedGet(ctx, directoryId)
|
||||
if (!directory) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
@@ -50,7 +50,7 @@ export async function fetch(
|
||||
)
|
||||
}
|
||||
|
||||
const path: ReverseFilePath = [
|
||||
const path: DirectoryPath = [
|
||||
{
|
||||
handle: newDirectoryHandle(directoryId),
|
||||
name: directory.name,
|
||||
@@ -58,7 +58,7 @@ export async function fetch(
|
||||
]
|
||||
let parentDirId = directory.parentId
|
||||
while (parentDirId) {
|
||||
const parentDir = await ctx.db.get(parentDirId)
|
||||
const parentDir = await authorizedGet(ctx, parentDirId)
|
||||
if (parentDir) {
|
||||
path.push({
|
||||
handle: newDirectoryHandle(parentDir._id),
|
||||
@@ -66,16 +66,19 @@ export async function fetch(
|
||||
})
|
||||
parentDirId = parentDir.parentId
|
||||
} else {
|
||||
throw Err.create(Err.Code.Internal)
|
||||
throw Err.create(Err.Code.DirectoryNotFound, "Parent directory not found")
|
||||
}
|
||||
}
|
||||
|
||||
return { ...directory, path: path.reverse() as FilePath }
|
||||
return { ...directory, path: path.reverse() as DirectoryPath }
|
||||
}
|
||||
|
||||
export async function fetchContent(
|
||||
ctx: AuthenticatedQueryCtx,
|
||||
{ directoryId }: { directoryId?: Id<"directories"> } = {},
|
||||
{
|
||||
directoryId,
|
||||
trashed,
|
||||
}: { directoryId?: Id<"directories">; trashed: boolean },
|
||||
): Promise<FileSystemItem[]> {
|
||||
let dirId: Id<"directories"> | undefined
|
||||
if (directoryId) {
|
||||
@@ -85,21 +88,33 @@ export async function fetchContent(
|
||||
const [files, directories] = await Promise.all([
|
||||
ctx.db
|
||||
.query("files")
|
||||
.withIndex("byDirectoryId", (q) =>
|
||||
q
|
||||
.withIndex("byDirectoryId", (q) => {
|
||||
if (trashed) {
|
||||
return q
|
||||
.eq("userId", ctx.user._id)
|
||||
.eq("directoryId", dirId)
|
||||
.gte("deletedAt", 0)
|
||||
}
|
||||
return q
|
||||
.eq("userId", ctx.user._id)
|
||||
.eq("directoryId", dirId)
|
||||
.eq("deletedAt", undefined),
|
||||
)
|
||||
.eq("deletedAt", undefined)
|
||||
})
|
||||
.collect(),
|
||||
ctx.db
|
||||
.query("directories")
|
||||
.withIndex("byParentId", (q) =>
|
||||
q
|
||||
.withIndex("byParentId", (q) => {
|
||||
if (trashed) {
|
||||
return q
|
||||
.eq("userId", ctx.user._id)
|
||||
.eq("parentId", dirId)
|
||||
.gte("deletedAt", 0)
|
||||
}
|
||||
return q
|
||||
.eq("userId", ctx.user._id)
|
||||
.eq("parentId", dirId)
|
||||
.eq("deletedAt", undefined),
|
||||
)
|
||||
.eq("deletedAt", undefined)
|
||||
})
|
||||
.collect(),
|
||||
])
|
||||
|
||||
@@ -118,7 +133,7 @@ export async function create(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
{ name, parentId }: { name: string; parentId: Id<"directories"> },
|
||||
): Promise<Id<"directories">> {
|
||||
const parentDir = await ctx.db.get(parentId)
|
||||
const parentDir = await authorizedGet(ctx, parentId)
|
||||
if (!parentDir) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
@@ -144,7 +159,7 @@ export async function create(
|
||||
)
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
const now = Date.now()
|
||||
return await ctx.db.insert("directories", {
|
||||
name,
|
||||
parentId,
|
||||
@@ -166,7 +181,7 @@ export async function move(
|
||||
) {
|
||||
const conflictCheckResults = await Promise.allSettled(
|
||||
sourceDirectories.map((directory) =>
|
||||
ctx.db.get(directory.id).then((d) => {
|
||||
authorizedGet(ctx, directory.id).then((d) => {
|
||||
if (!d) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
@@ -216,7 +231,10 @@ export async function move(
|
||||
ignoredHandles.add(handle)
|
||||
} else {
|
||||
promises.push(
|
||||
ctx.db.patch(handle.id, { parentId: targetDirectory.id }),
|
||||
ctx.db.patch(handle.id, {
|
||||
parentId: targetDirectory.id,
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -239,7 +257,7 @@ export async function moveToTrashRecursive(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
handle: DirectoryHandle,
|
||||
): Promise<void> {
|
||||
const now = new Date().toISOString()
|
||||
const now = Date.now()
|
||||
|
||||
const filesToDelete: Id<"files">[] = []
|
||||
const directoriesToDelete: Id<"directories">[] = []
|
||||
@@ -289,3 +307,86 @@ export async function moveToTrashRecursive(
|
||||
|
||||
await Promise.all([...filePatches, ...directoryPatches])
|
||||
}
|
||||
|
||||
export async function deletePermanently(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
{
|
||||
items,
|
||||
}: {
|
||||
items: DirectoryHandle[]
|
||||
},
|
||||
) {
|
||||
if (items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const itemsToBeDeleted = await Promise.allSettled(
|
||||
items.map((item) => ctx.db.get(item.id)),
|
||||
).then((results) =>
|
||||
results.filter(
|
||||
(result): result is PromiseFulfilledResult<Doc<"directories">> =>
|
||||
result.status === "fulfilled" && result.value !== null,
|
||||
),
|
||||
)
|
||||
|
||||
const deleteDirectoryPromises = itemsToBeDeleted.map((item) =>
|
||||
ctx.db.delete(item.value._id),
|
||||
)
|
||||
|
||||
const deleteResults = await Promise.allSettled(deleteDirectoryPromises)
|
||||
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
let successfulDeletions = 0
|
||||
for (const result of deleteResults) {
|
||||
if (result.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
} else {
|
||||
successfulDeletions += 1
|
||||
}
|
||||
}
|
||||
|
||||
return { deleted: successfulDeletions, errors }
|
||||
}
|
||||
|
||||
export async function restore(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
{
|
||||
items,
|
||||
}: {
|
||||
items: DirectoryHandle[]
|
||||
},
|
||||
) {
|
||||
if (items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const itemsToBeRestored = await Promise.allSettled(
|
||||
items.map((item) => ctx.db.get(item.id)),
|
||||
).then((results) =>
|
||||
results.filter(
|
||||
(result): result is PromiseFulfilledResult<Doc<"directories">> =>
|
||||
result.status === "fulfilled" && result.value !== null,
|
||||
),
|
||||
)
|
||||
|
||||
const restoreDirectoryPromises = itemsToBeRestored.map((item) =>
|
||||
ctx.db.patch(item.value._id, {
|
||||
deletedAt: undefined,
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
)
|
||||
|
||||
const restoreResults = await Promise.allSettled(restoreDirectoryPromises)
|
||||
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
let successfulRestorations = 0
|
||||
for (const result of restoreResults) {
|
||||
if (result.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
} else {
|
||||
successfulRestorations += 1
|
||||
}
|
||||
}
|
||||
|
||||
return { restored: successfulRestorations, errors }
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum Code {
|
||||
FileNotFound = "FileNotFound",
|
||||
Internal = "Internal",
|
||||
Unauthenticated = "Unauthenticated",
|
||||
NotFound = "NotFound",
|
||||
}
|
||||
|
||||
export type ApplicationErrorData = { code: Code; message?: string }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Id } from "../_generated/dataModel"
|
||||
import type { AuthenticatedMutationCtx } from "../functions"
|
||||
import type { Doc, Id } from "../_generated/dataModel"
|
||||
import { type AuthenticatedMutationCtx, authorizedGet } from "../functions"
|
||||
import * as Err from "./error"
|
||||
import type { DirectoryHandle, FileHandle } from "./filesystem"
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function renameFile(
|
||||
)
|
||||
}
|
||||
|
||||
await ctx.db.patch(itemId, { name: newName })
|
||||
await ctx.db.patch(itemId, { name: newName, updatedAt: Date.now() })
|
||||
}
|
||||
|
||||
export async function move(
|
||||
@@ -48,7 +48,7 @@ export async function move(
|
||||
) {
|
||||
const conflictCheckResults = await Promise.allSettled(
|
||||
items.map((fileHandle) =>
|
||||
ctx.db.get(fileHandle.id).then((f) => {
|
||||
authorizedGet(ctx, fileHandle.id).then((f) => {
|
||||
if (!f) {
|
||||
throw Err.create(
|
||||
Err.Code.FileNotFound,
|
||||
@@ -90,7 +90,10 @@ export async function move(
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
okFiles.map((handle) =>
|
||||
ctx.db.patch(handle.id, { directoryId: targetDirectoryHandle.id }),
|
||||
ctx.db.patch(handle.id, {
|
||||
directoryId: targetDirectoryHandle.id,
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -102,3 +105,89 @@ export async function move(
|
||||
|
||||
return { moved: okFiles, errors }
|
||||
}
|
||||
|
||||
export async function deletePermanently(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
{
|
||||
items,
|
||||
}: {
|
||||
items: FileHandle[]
|
||||
},
|
||||
) {
|
||||
if (items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const itemsToBeDeleted = await Promise.allSettled(
|
||||
items.map((item) => ctx.db.get(item.id)),
|
||||
).then((results) =>
|
||||
results.filter(
|
||||
(result): result is PromiseFulfilledResult<Doc<"files">> =>
|
||||
result.status === "fulfilled" && result.value !== null,
|
||||
),
|
||||
)
|
||||
|
||||
const deleteFilePromises = itemsToBeDeleted.map((item) =>
|
||||
Promise.all([
|
||||
ctx.db.delete(item.value._id),
|
||||
ctx.storage.delete(item.value.storageId),
|
||||
]),
|
||||
)
|
||||
|
||||
const deleteResults = await Promise.allSettled(deleteFilePromises)
|
||||
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
let successfulDeletions = 0
|
||||
for (const result of deleteResults) {
|
||||
if (result.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
} else {
|
||||
successfulDeletions += 1
|
||||
}
|
||||
}
|
||||
|
||||
return { deleted: successfulDeletions, errors }
|
||||
}
|
||||
|
||||
export async function restore(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
{
|
||||
items,
|
||||
}: {
|
||||
items: FileHandle[]
|
||||
},
|
||||
) {
|
||||
if (items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const itemsToBeRestored = await Promise.allSettled(
|
||||
items.map((item) => ctx.db.get(item.id)),
|
||||
).then((results) =>
|
||||
results.filter(
|
||||
(result): result is PromiseFulfilledResult<Doc<"files">> =>
|
||||
result.status === "fulfilled" && result.value !== null,
|
||||
),
|
||||
)
|
||||
|
||||
const restoreFilePromises = itemsToBeRestored.map((item) =>
|
||||
ctx.db.patch(item.value._id, {
|
||||
deletedAt: undefined,
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
)
|
||||
|
||||
const restoreResults = await Promise.allSettled(restoreFilePromises)
|
||||
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
let successfulRestorations = 0
|
||||
for (const result of restoreResults) {
|
||||
if (result.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
} else {
|
||||
successfulRestorations += 1
|
||||
}
|
||||
}
|
||||
|
||||
return { restored: successfulRestorations, errors }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { v } from "convex/values"
|
||||
import type { Doc, Id } from "../_generated/dataModel"
|
||||
import type {
|
||||
AuthenticatedMutationCtx,
|
||||
AuthenticatedQueryCtx,
|
||||
} from "../functions"
|
||||
import { authorizedGet } from "../functions"
|
||||
import * as Directories from "./directories"
|
||||
import * as Err from "./error"
|
||||
import * as Files from "./files"
|
||||
|
||||
export enum FileType {
|
||||
File = "File",
|
||||
@@ -26,6 +34,10 @@ export type FilePathComponent = {
|
||||
name: string
|
||||
}
|
||||
export type PathComponent = FilePathComponent | DirectoryPathComponent
|
||||
export type DirectoryPath = [
|
||||
DirectoryPathComponent,
|
||||
...DirectoryPathComponent[],
|
||||
]
|
||||
export type FilePath = [...DirectoryPathComponent[], PathComponent]
|
||||
export type ReverseFilePath = [PathComponent, ...DirectoryPathComponent[]]
|
||||
|
||||
@@ -39,6 +51,14 @@ export type FileHandle = {
|
||||
}
|
||||
export type FileSystemHandle = DirectoryHandle | FileHandle
|
||||
|
||||
export type DeleteResult = {
|
||||
deleted: {
|
||||
files: number
|
||||
directories: number
|
||||
}
|
||||
errors: Err.ApplicationErrorData[]
|
||||
}
|
||||
|
||||
export function newFileSystemHandle(item: FileSystemItem): FileSystemHandle {
|
||||
console.log("item", item)
|
||||
switch (item.kind) {
|
||||
@@ -73,3 +93,223 @@ export const VFileHandle = v.object({
|
||||
id: v.id("files"),
|
||||
})
|
||||
export const VFileSystemHandle = v.union(VFileHandle, VDirectoryHandle)
|
||||
|
||||
export async function queryRootDirectory(
|
||||
ctx: AuthenticatedQueryCtx,
|
||||
): Promise<Doc<"directories"> | null> {
|
||||
return await ctx.db
|
||||
.query("directories")
|
||||
.withIndex("byParentId", (q) =>
|
||||
q.eq("userId", ctx.user._id).eq("parentId", undefined),
|
||||
)
|
||||
.first()
|
||||
}
|
||||
|
||||
export async function ensureRootDirectory(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
): Promise<Id<"directories">> {
|
||||
const existing = await queryRootDirectory(ctx)
|
||||
|
||||
if (existing) {
|
||||
return existing._id
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
return await ctx.db.insert("directories", {
|
||||
name: "",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
userId: ctx.user._id,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collects all file and directory handles from the given handles,
|
||||
* including all nested items. Only includes items that are in trash (deletedAt >= 0).
|
||||
*/
|
||||
async function collectAllHandlesRecursively(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
{ handles }: { handles: FileSystemHandle[] },
|
||||
): Promise<{ fileHandles: FileHandle[]; directoryHandles: DirectoryHandle[] }> {
|
||||
const fileHandles: FileHandle[] = []
|
||||
const directoryHandles: DirectoryHandle[] = []
|
||||
|
||||
// Process each handle to collect files and directories
|
||||
for (const handle of handles) {
|
||||
// Use a queue to process items iteratively instead of recursively
|
||||
const queue: FileSystemHandle[] = [handle]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentHandle = queue.shift()!
|
||||
|
||||
// Add current item to appropriate collection
|
||||
if (currentHandle.kind === FileType.File) {
|
||||
fileHandles.push(currentHandle)
|
||||
} else {
|
||||
directoryHandles.push(currentHandle)
|
||||
}
|
||||
|
||||
// If it's a directory, collect all children and add them to the queue
|
||||
if (currentHandle.kind === FileType.Directory) {
|
||||
// Get all child directories that are in trash (deletedAt >= 0)
|
||||
const childDirectories = await ctx.db
|
||||
.query("directories")
|
||||
.withIndex("byParentId", (q) =>
|
||||
q
|
||||
.eq("userId", ctx.user._id)
|
||||
.eq("parentId", currentHandle.id)
|
||||
.gte("deletedAt", 0),
|
||||
)
|
||||
.collect()
|
||||
|
||||
// Get all child files that are in trash (deletedAt >= 0)
|
||||
const childFiles = await ctx.db
|
||||
.query("files")
|
||||
.withIndex("byDirectoryId", (q) =>
|
||||
q
|
||||
.eq("userId", ctx.user._id)
|
||||
.eq("directoryId", currentHandle.id)
|
||||
.gte("deletedAt", 0),
|
||||
)
|
||||
.collect()
|
||||
|
||||
// Add child directories to queue for processing
|
||||
for (const childDir of childDirectories) {
|
||||
const childHandle = newDirectoryHandle(childDir._id)
|
||||
queue.push(childHandle)
|
||||
}
|
||||
|
||||
// Add child files to file handles collection
|
||||
for (const childFile of childFiles) {
|
||||
const childFileHandle = newFileHandle(childFile._id)
|
||||
fileHandles.push(childFileHandle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { fileHandles, directoryHandles }
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores deleted items by unsetting the deletedAt field recursively.
|
||||
* This includes all nested files and directories within the given handles.
|
||||
*/
|
||||
export async function restoreItems(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
{ handles }: { handles: FileSystemHandle[] },
|
||||
) {
|
||||
// Collect all items to restore (including nested items)
|
||||
const { fileHandles, directoryHandles } =
|
||||
await collectAllHandlesRecursively(ctx, { handles })
|
||||
|
||||
// Restore files and directories by unsetting deletedAt
|
||||
const [filesResult, directoriesResult] = await Promise.all([
|
||||
Files.restore(ctx, { items: fileHandles }),
|
||||
Directories.restore(ctx, { items: directoryHandles }),
|
||||
])
|
||||
|
||||
// Combine results, handling null responses
|
||||
return {
|
||||
restored: {
|
||||
files: filesResult?.restored || 0,
|
||||
directories: directoriesResult?.restored || 0,
|
||||
},
|
||||
errors: [
|
||||
...(filesResult?.errors || []),
|
||||
...(directoriesResult?.errors || []),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteItemsPermanently(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
{ handles }: { handles: FileSystemHandle[] },
|
||||
): Promise<DeleteResult> {
|
||||
// Collect all items to delete (including nested items)
|
||||
const {
|
||||
fileHandles: fileHandlesToDelete,
|
||||
directoryHandles: directoryHandlesToDelete,
|
||||
} = await collectAllHandlesRecursively(ctx, { handles })
|
||||
|
||||
// Delete files and directories using their respective models
|
||||
const [filesResult, directoriesResult] = await Promise.all([
|
||||
Files.deletePermanently(ctx, { items: fileHandlesToDelete }),
|
||||
Directories.deletePermanently(ctx, { items: directoryHandlesToDelete }),
|
||||
])
|
||||
|
||||
// Combine results, handling null responses
|
||||
return {
|
||||
deleted: {
|
||||
files: filesResult?.deleted || 0,
|
||||
directories: directoriesResult?.deleted || 0,
|
||||
},
|
||||
errors: [
|
||||
...(filesResult?.errors || []),
|
||||
...(directoriesResult?.errors || []),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export async function emptyTrash(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
): Promise<DeleteResult> {
|
||||
const rootDir = await queryRootDirectory(ctx)
|
||||
if (!rootDir) {
|
||||
throw Err.create(Err.Code.NotFound, "user root directory not found")
|
||||
}
|
||||
|
||||
const dirs = await ctx.db
|
||||
.query("directories")
|
||||
.withIndex("byParentId", (q) =>
|
||||
q
|
||||
.eq("userId", ctx.user._id)
|
||||
.eq("parentId", rootDir._id)
|
||||
.gte("deletedAt", 0),
|
||||
)
|
||||
.collect()
|
||||
|
||||
const files = await ctx.db
|
||||
.query("files")
|
||||
.withIndex("byDirectoryId", (q) =>
|
||||
q
|
||||
.eq("userId", ctx.user._id)
|
||||
.eq("directoryId", rootDir._id)
|
||||
.gte("deletedAt", 0),
|
||||
)
|
||||
.collect()
|
||||
|
||||
if (dirs.length === 0 && files.length === 0) {
|
||||
return {
|
||||
deleted: {
|
||||
files: 0,
|
||||
directories: 0,
|
||||
},
|
||||
errors: [],
|
||||
}
|
||||
}
|
||||
|
||||
return await deleteItemsPermanently(ctx, {
|
||||
handles: [
|
||||
...dirs.map((it) => newDirectoryHandle(it._id)),
|
||||
...files.map((it) => newFileHandle(it._id)),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchFileUrl(
|
||||
ctx: AuthenticatedQueryCtx,
|
||||
{ fileId }: { fileId: Id<"files"> },
|
||||
): Promise<string> {
|
||||
const file = await authorizedGet(ctx, fileId)
|
||||
if (!file) {
|
||||
throw Err.create(Err.Code.NotFound, "file not found")
|
||||
}
|
||||
|
||||
const url = await ctx.storage.getUrl(file.storageId)
|
||||
if (!url) {
|
||||
throw Err.create(Err.Code.NotFound, "file not found")
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -1,54 +1,25 @@
|
||||
import type { MutationCtx, QueryCtx } from "../_generated/server"
|
||||
import type { AuthenticatedMutationCtx } from "../functions"
|
||||
import { authComponent } from "../auth"
|
||||
import * as Err from "./error"
|
||||
|
||||
export type AuthUser = Awaited<ReturnType<typeof authComponent.getAuthUser>>
|
||||
|
||||
/**
|
||||
* Get the current authenticated user identity
|
||||
* Throws an error if the user is not authenticated */
|
||||
export async function userIdentityOrThrow(ctx: QueryCtx | MutationCtx) {
|
||||
const identity = await ctx.auth.getUserIdentity()
|
||||
|
||||
if (!identity) {
|
||||
throw Err.create(Err.Code.Unauthenticated, "Not authenticated")
|
||||
}
|
||||
|
||||
return identity
|
||||
}
|
||||
|
||||
/**
|
||||
* Get internal user document from JWT authentication
|
||||
* Get user document from JWT authentication
|
||||
* Throws an error if the user is not authenticated
|
||||
*/
|
||||
export async function userOrThrow(ctx: QueryCtx | MutationCtx) {
|
||||
const identity = await userIdentityOrThrow(ctx)
|
||||
|
||||
// Look for existing user by JWT subject
|
||||
const user = await ctx.db
|
||||
.query("users")
|
||||
.withIndex("byJwtSubject", (q) => q.eq("jwtSubject", identity.subject))
|
||||
.first()
|
||||
|
||||
if (!user) {
|
||||
throw Err.create(
|
||||
Err.Code.Unauthenticated,
|
||||
"User not found - please sync user first",
|
||||
)
|
||||
}
|
||||
|
||||
const user = await authComponent.getAuthUser(ctx)
|
||||
return user
|
||||
}
|
||||
|
||||
export async function register(ctx: AuthenticatedMutationCtx) {
|
||||
const now = new Date().toISOString()
|
||||
await Promise.all([
|
||||
ctx.db.insert("users", {
|
||||
jwtSubject: ctx.identity.subject,
|
||||
}),
|
||||
ctx.db.insert("directories", {
|
||||
name: "",
|
||||
userId: ctx.user._id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
"convex": "^1.27.0"
|
||||
"better-auth": "1.3.8",
|
||||
"convex": "^1.27.0",
|
||||
"@convex-dev/better-auth": "^0.8.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,22 @@ import { defineSchema, defineTable } from "convex/server"
|
||||
import { v } from "convex/values"
|
||||
|
||||
const schema = defineSchema({
|
||||
users: defineTable({
|
||||
jwtSubject: v.string(),
|
||||
}).index("byJwtSubject", ["jwtSubject"]),
|
||||
files: defineTable({
|
||||
storageId: v.id("_storage"),
|
||||
userId: v.id("users"),
|
||||
userId: v.string(), // BetterAuth user IDs are strings, not Convex Ids
|
||||
directoryId: v.optional(v.id("directories")),
|
||||
name: v.string(),
|
||||
size: v.number(),
|
||||
mimeType: v.optional(v.string()),
|
||||
createdAt: v.string(),
|
||||
updatedAt: v.string(),
|
||||
deletedAt: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
deletedAt: v.optional(v.number()),
|
||||
lastAccessedAt: v.optional(v.number()),
|
||||
})
|
||||
.index("byDirectoryId", ["userId", "directoryId", "deletedAt"])
|
||||
.index("byUserId", ["userId", "deletedAt"])
|
||||
.index("byDeletedAt", ["deletedAt"])
|
||||
.index("byLastAccessedAt", ["userId", "lastAccessedAt"])
|
||||
.index("uniqueFileInDirectory", [
|
||||
"userId",
|
||||
"directoryId",
|
||||
@@ -27,11 +26,11 @@ const schema = defineSchema({
|
||||
]),
|
||||
directories: defineTable({
|
||||
name: v.string(),
|
||||
userId: v.id("users"),
|
||||
userId: v.string(), // BetterAuth user IDs are strings, not Convex Ids
|
||||
parentId: v.optional(v.id("directories")),
|
||||
createdAt: v.string(),
|
||||
updatedAt: v.string(),
|
||||
deletedAt: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
deletedAt: v.optional(v.number()),
|
||||
})
|
||||
.index("byUserId", ["userId", "deletedAt"])
|
||||
.index("byParentId", ["userId", "parentId", "deletedAt"])
|
||||
|
||||
25
packages/convex/tsconfig.json
Normal file
25
packages/convex/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
/* This TypeScript project config describes the environment that
|
||||
* Convex functions run in and is used to typecheck them.
|
||||
* You can modify it, but some settings are required to use Convex.
|
||||
*/
|
||||
"compilerOptions": {
|
||||
/* These settings are not required by Convex and can be modified. */
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
/* These compiler options are required by Convex */
|
||||
"target": "ESNext",
|
||||
"lib": ["ES2021", "dom"],
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["./**/*"],
|
||||
"exclude": ["./_generated"]
|
||||
}
|
||||
8
packages/convex/user.ts
Normal file
8
packages/convex/user.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { authenticatedMutation } from "./functions"
|
||||
import * as FileSystem from "./model/filesystem"
|
||||
|
||||
export const ensureRootDirectory = authenticatedMutation({
|
||||
handler: async (ctx) => {
|
||||
return await FileSystem.ensureRootDirectory(ctx)
|
||||
},
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { mutation } from "./_generated/server"
|
||||
import { authenticatedQuery } from "./functions"
|
||||
import * as Err from "./model/error"
|
||||
|
||||
export const getCurrentUser = authenticatedQuery({
|
||||
handler: async (ctx) => {
|
||||
// ctx.user is the internal Convex user document
|
||||
return ctx.user
|
||||
},
|
||||
})
|
||||
|
||||
export const syncUser = mutation({
|
||||
handler: async (ctx) => {
|
||||
const identity = await ctx.auth.getUserIdentity()
|
||||
if (!identity) {
|
||||
throw Err.create(Err.Code.Unauthenticated)
|
||||
}
|
||||
|
||||
const existingUser = await ctx.db
|
||||
.query("users")
|
||||
.withIndex("byJwtSubject", (q) =>
|
||||
q.eq("jwtSubject", identity.subject),
|
||||
)
|
||||
.first()
|
||||
|
||||
if (!existingUser) {
|
||||
await ctx.db.insert("users", {
|
||||
jwtSubject: identity.subject,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,29 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
|
||||
20
packages/web/bun-env.d.ts
vendored
20
packages/web/bun-env.d.ts
vendored
@@ -1,17 +1,17 @@
|
||||
// Generated by `bun init`
|
||||
|
||||
declare module "*.svg" {
|
||||
/**
|
||||
* A path to the SVG file
|
||||
*/
|
||||
const path: `${string}.svg`;
|
||||
export = path;
|
||||
/**
|
||||
* A path to the SVG file
|
||||
*/
|
||||
const path: `${string}.svg`
|
||||
export = path
|
||||
}
|
||||
|
||||
declare module "*.module.css" {
|
||||
/**
|
||||
* A record of class names to their corresponding CSS module classes
|
||||
*/
|
||||
const classes: { readonly [key: string]: string };
|
||||
export = classes;
|
||||
/**
|
||||
* A record of class names to their corresponding CSS module classes
|
||||
*/
|
||||
const classes: { readonly [key: string]: string }
|
||||
export = classes
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
"format": "biome format --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@convex-dev/workos": "^0.0.1",
|
||||
"@convex-dev/better-auth": "^0.8.9",
|
||||
"@fileone/convex": "workspace:*",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
@@ -23,15 +25,19 @@
|
||||
"@tanstack/react-router": "^1.131.41",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/router-devtools": "^1.131.42",
|
||||
"@workos-inc/authkit-react": "^0.12.0",
|
||||
"better-auth": "1.3.8",
|
||||
"bun-plugin-tailwind": "latest",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"convex": "^1.27.0",
|
||||
"convex-helpers": "^0.1.104",
|
||||
"jotai": "^2.14.0",
|
||||
"jotai-effect": "^2.1.3",
|
||||
"jotai-scope": "^0.9.5",
|
||||
"jotai-tanstack-query": "^0.11.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"motion": "^12.23.16",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
|
||||
33
packages/web/src/auth.ts
Normal file
33
packages/web/src/auth.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
convexClient,
|
||||
crossDomainClient,
|
||||
} from "@convex-dev/better-auth/client/plugins"
|
||||
import { createAuthClient } from "better-auth/react"
|
||||
import { createContext, useContext } from "react"
|
||||
|
||||
export type AuthErrorCode = keyof typeof authClient.$ERROR_CODES
|
||||
|
||||
export class BetterAuthError extends Error {
|
||||
constructor(public readonly errorCode: AuthErrorCode) {
|
||||
super(`better-auth error: ${errorCode}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env.BUN_PUBLIC_CONVEX_SITE_URL,
|
||||
plugins: [convexClient(), crossDomainClient()],
|
||||
})
|
||||
|
||||
export type Session = NonNullable<
|
||||
Awaited<ReturnType<typeof authClient.useSession>>["data"]
|
||||
>
|
||||
|
||||
export const SessionContext = createContext<Session | null>(null)
|
||||
|
||||
export function useSession() {
|
||||
const context = useContext(SessionContext)
|
||||
if (!context) {
|
||||
throw new Error("useSession must be used within a SessionProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -1,109 +1,109 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ function Button({
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
disabled={loading || props.disabled}
|
||||
disabled={props.disabled}
|
||||
{...props}
|
||||
>
|
||||
{asChild ? (
|
||||
|
||||
92
packages/web/src/components/ui/card.tsx
Normal file
92
packages/web/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,
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
|
||||
@@ -36,7 +36,7 @@ function DialogOverlay({
|
||||
<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 backdrop-blur-xs",
|
||||
"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/20",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
242
packages/web/src/components/ui/field.tsx
Normal file
242
packages/web/src/components/ui/field.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import { useMemo } from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||
return (
|
||||
<fieldset
|
||||
data-slot="field-set"
|
||||
className={cn(
|
||||
"flex flex-col gap-6",
|
||||
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldLegend({
|
||||
className,
|
||||
variant = "legend",
|
||||
...props
|
||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
||||
return (
|
||||
<legend
|
||||
data-slot="field-legend"
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"mb-3 font-medium",
|
||||
"data-[variant=legend]:text-base",
|
||||
"data-[variant=label]:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-group"
|
||||
className={cn(
|
||||
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const fieldVariants = cva(
|
||||
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
||||
horizontal: [
|
||||
"flex-row items-center",
|
||||
"[&>[data-slot=field-label]]:flex-auto",
|
||||
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
],
|
||||
responsive: [
|
||||
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
|
||||
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
||||
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "vertical",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Field({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="field"
|
||||
data-orientation={orientation}
|
||||
className={cn(fieldVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-content"
|
||||
className={cn(
|
||||
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Label>) {
|
||||
return (
|
||||
<Label
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
||||
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<p
|
||||
data-slot="field-description"
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
||||
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-separator"
|
||||
data-content={!!children}
|
||||
className={cn(
|
||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Separator className="absolute inset-0 top-1/2" />
|
||||
{children && (
|
||||
<span
|
||||
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
||||
data-slot="field-separator-content"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldError({
|
||||
className,
|
||||
children,
|
||||
errors,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
errors?: Array<{ message?: string } | undefined>
|
||||
}) {
|
||||
const content = useMemo(() => {
|
||||
if (children) {
|
||||
return children
|
||||
}
|
||||
|
||||
if (!errors) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (errors?.length === 1 && errors[0]?.message) {
|
||||
return errors[0].message
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||
{errors.map(
|
||||
(error, index) =>
|
||||
error?.message && <li key={index}>{error.message}</li>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}, [children, errors])
|
||||
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
data-slot="field-error"
|
||||
className={cn("text-destructive text-sm font-normal", className)}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldContent,
|
||||
FieldTitle,
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import * as React from "react"
|
||||
import type * 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 flex 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}
|
||||
/>
|
||||
)
|
||||
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 flex 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 }
|
||||
|
||||
22
packages/web/src/components/ui/label.tsx
Normal file
22
packages/web/src/components/ui/label.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
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 }
|
||||
29
packages/web/src/components/ui/progress.tsx
Normal file
29
packages/web/src/components/ui/progress.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
|
||||
@@ -1,139 +1,139 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
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}
|
||||
/>
|
||||
)
|
||||
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
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
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>
|
||||
)
|
||||
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}
|
||||
/>
|
||||
)
|
||||
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}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
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,
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ function SidebarProvider({
|
||||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
}, [isMobile, setOpen])
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
@@ -124,15 +124,7 @@ function SidebarProvider({
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
],
|
||||
[state, open, setOpen, isMobile, openMobile, toggleSidebar],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
|
||||
@@ -7,7 +7,7 @@ export function WithAtom<Value>({
|
||||
atom: PrimitiveAtom<Value>
|
||||
children: (
|
||||
value: Value,
|
||||
setValue: (value: Value) => void,
|
||||
setValue: (value: Value | ((current: Value) => Value)) => void,
|
||||
) => React.ReactNode
|
||||
}) {
|
||||
const [value, setValue] = useAtom(atom)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import { Link, useLocation } from "@tanstack/react-router"
|
||||
import { useAuth } from "@workos-inc/authkit-react"
|
||||
import { useQuery as useConvexQuery } from "convex/react"
|
||||
import { useAtomValue } from "jotai"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
FilesIcon,
|
||||
HomeIcon,
|
||||
LogOutIcon,
|
||||
SettingsIcon,
|
||||
TrashIcon,
|
||||
User2Icon,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
@@ -18,12 +18,16 @@ import {
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { LoadingSpinner } from "../components/ui/loading-spinner"
|
||||
import { backgroundTaskProgressAtom } from "./state"
|
||||
|
||||
export function DashboardSidebar() {
|
||||
return (
|
||||
@@ -34,9 +38,17 @@ export function DashboardSidebar() {
|
||||
<UserMenu />
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<MainSidebarMenu />
|
||||
</SidebarHeader>
|
||||
<SidebarRail />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<MainSidebarMenu />
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarMenu>
|
||||
<BackgroundTaskProgressItem />
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
@@ -62,6 +74,7 @@ function MainSidebarMenu() {
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<AllFilesItem />
|
||||
<TrashItem />
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
@@ -76,9 +89,7 @@ function AllFilesItem() {
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={location.pathname.startsWith(
|
||||
`/directories/${rootDirectory._id}`,
|
||||
)}
|
||||
isActive={location.pathname.startsWith("/directories")}
|
||||
>
|
||||
<Link to={`/directories/${rootDirectory._id}`}>
|
||||
<FilesIcon />
|
||||
@@ -89,22 +100,58 @@ function AllFilesItem() {
|
||||
)
|
||||
}
|
||||
|
||||
function UserMenu() {
|
||||
const { signOut } = useAuth()
|
||||
function TrashItem() {
|
||||
const location = useLocation()
|
||||
const rootDirectory = useConvexQuery(api.files.fetchRootDirectory)
|
||||
|
||||
function handleSignOut() {
|
||||
signOut()
|
||||
}
|
||||
if (!rootDirectory) return null
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={location.pathname.startsWith("/trash/directories")}
|
||||
>
|
||||
<Link to={`/trash/directories/${rootDirectory._id}`}>
|
||||
<TrashIcon />
|
||||
<span>Trash</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
function BackgroundTaskProgressItem() {
|
||||
const backgroundTaskProgress = useAtomValue(backgroundTaskProgressAtom)
|
||||
|
||||
if (!backgroundTaskProgress) return null
|
||||
|
||||
return (
|
||||
<SidebarMenuItem className="flex items-center gap-2 opacity-80 text-sm">
|
||||
<LoadingSpinner />
|
||||
{backgroundTaskProgress.label}
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
function UserMenu() {
|
||||
function handleSignOut() {}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton className="w-fit px-1.5 group-data-[collapsible=icon]:px-1.5! data-[state=open]:bg-sidebar-accent">
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-5 items-center justify-center rounded-md">
|
||||
<User2Icon className="size-3" />
|
||||
</div>
|
||||
<span className="truncate font-medium">Kenneth</span>
|
||||
<ChevronDownIcon className="opacity-50" />
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<a href="/">
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||
<User2Icon className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">
|
||||
Acme Inc
|
||||
</span>
|
||||
<span className="truncate text-xs">Enterprise</span>
|
||||
</div>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64" align="start" side="bottom">
|
||||
|
||||
9
packages/web/src/dashboard/state.ts
Normal file
9
packages/web/src/dashboard/state.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { atom } from "jotai"
|
||||
|
||||
type BackgroundTaskProgress = {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const backgroundTaskProgressAtom = atom<BackgroundTaskProgress | null>(
|
||||
null,
|
||||
)
|
||||
@@ -0,0 +1,116 @@
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import { newFileSystemHandle } from "@fileone/convex/model/filesystem"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useMutation as useContextMutation } from "convex/react"
|
||||
import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
||||
import { TextCursorInputIcon, TrashIcon } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu"
|
||||
import {
|
||||
contextMenuTargeItemsAtom,
|
||||
itemBeingRenamedAtom,
|
||||
optimisticDeletedItemsAtom,
|
||||
} from "./state"
|
||||
|
||||
export function DirectoryContentContextMenu({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const store = useStore()
|
||||
const [target, setTarget] = useAtom(contextMenuTargeItemsAtom)
|
||||
const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom)
|
||||
const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash)
|
||||
const { mutate: moveToTrash } = useMutation({
|
||||
mutationFn: moveToTrashMutation,
|
||||
onMutate: ({ handles }) => {
|
||||
setOptimisticDeletedItems(
|
||||
(prev) =>
|
||||
new Set([...prev, ...handles.map((handle) => handle.id)]),
|
||||
)
|
||||
},
|
||||
onSuccess: ({ deleted, errors }, { handles }) => {
|
||||
setOptimisticDeletedItems((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
for (const handle of handles) {
|
||||
newSet.delete(handle.id)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
if (errors.length === 0 && deleted.length === handles.length) {
|
||||
toast.success(`Moved ${handles.length} items to trash`)
|
||||
} else if (errors.length === handles.length) {
|
||||
toast.error("Failed to move to trash")
|
||||
} else {
|
||||
toast.info(
|
||||
`Moved ${deleted.length} items to trash; failed to move ${errors.length} items`,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const handleDelete = () => {
|
||||
const selectedItems = store.get(contextMenuTargeItemsAtom)
|
||||
if (selectedItems.length > 0) {
|
||||
moveToTrash({
|
||||
handles: selectedItems.map(newFileSystemHandle),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setTarget([])
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||
{target && (
|
||||
<ContextMenuContent>
|
||||
<RenameMenuItem />
|
||||
<ContextMenuItem onClick={handleDelete}>
|
||||
<TrashIcon />
|
||||
Move to trash
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function RenameMenuItem() {
|
||||
const store = useStore()
|
||||
const target = useAtomValue(contextMenuTargeItemsAtom)
|
||||
const setItemBeingRenamed = useSetAtom(itemBeingRenamedAtom)
|
||||
|
||||
const handleRename = () => {
|
||||
const selectedItems = store.get(contextMenuTargeItemsAtom)
|
||||
if (selectedItems.length === 1) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: length is checked
|
||||
const selectedItem = selectedItems[0]!
|
||||
setItemBeingRenamed({
|
||||
originalItem: selectedItem,
|
||||
name: selectedItem.doc.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Only render if exactly one item is selected
|
||||
if (target.length !== 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuItem onClick={handleRename}>
|
||||
<TextCursorInputIcon />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import type { Doc } from "@fileone/convex/_generated/dataModel"
|
||||
import {
|
||||
type DirectoryHandle,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
newFileHandle,
|
||||
newFileSystemHandle,
|
||||
} from "@fileone/convex/model/filesystem"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { Link, useNavigate } from "@tanstack/react-router"
|
||||
import {
|
||||
type ColumnDef,
|
||||
@@ -21,19 +19,10 @@ import {
|
||||
type Table as TableType,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { useMutation as useContextMutation } from "convex/react"
|
||||
import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
||||
import { TextCursorInputIcon, TrashIcon } from "lucide-react"
|
||||
import { useContext, useEffect, useRef } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { type PrimitiveAtom, useSetAtom, useStore } from "jotai"
|
||||
import { useContext, useEffect, useMemo, useRef } from "react"
|
||||
import { DirectoryIcon } from "@/components/icons/directory-icon"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -47,16 +36,20 @@ import {
|
||||
keyboardModifierAtom,
|
||||
} from "@/lib/keyboard"
|
||||
import { TextFileIcon } from "../../components/icons/text-file-icon"
|
||||
import { useFileDrop } from "../../files/use-file-drop"
|
||||
import { type FileDragInfo, useFileDrop } from "../../files/use-file-drop"
|
||||
import { cn } from "../../lib/utils"
|
||||
import { DirectoryPageContext } from "./context"
|
||||
import {
|
||||
contextMenuTargeItemsAtom,
|
||||
dragInfoAtom,
|
||||
itemBeingRenamedAtom,
|
||||
openedFileAtom,
|
||||
optimisticDeletedItemsAtom,
|
||||
} from "./state"
|
||||
|
||||
type DirectoryContentTableProps = {
|
||||
filterFn: (item: FileSystemItem) => boolean
|
||||
directoryUrlFn: (directory: Doc<"directories">) => string
|
||||
fileDragInfoAtom: PrimitiveAtom<FileDragInfo | null>
|
||||
onContextMenu: (
|
||||
row: Row<FileSystemItem>,
|
||||
table: TableType<FileSystemItem>,
|
||||
) => void
|
||||
onOpenFile: (file: Doc<"files">) => void
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return "0 B"
|
||||
@@ -68,180 +61,113 @@ function formatFileSize(bytes: number): string {
|
||||
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
const columns: ColumnDef<FileSystemItem>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => {
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onCheckedChange={row.getToggleSelectedHandler()}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 24,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "doc.name",
|
||||
cell: ({ row }) => {
|
||||
switch (row.original.kind) {
|
||||
case FileType.File:
|
||||
return <FileNameCell file={row.original.doc} />
|
||||
case FileType.Directory:
|
||||
return <DirectoryNameCell directory={row.original.doc} />
|
||||
}
|
||||
},
|
||||
size: 1000,
|
||||
},
|
||||
{
|
||||
header: "Size",
|
||||
accessorKey: "size",
|
||||
cell: ({ row }) => {
|
||||
switch (row.original.kind) {
|
||||
case FileType.File:
|
||||
return <div>{formatFileSize(row.original.doc.size)}</div>
|
||||
case FileType.Directory:
|
||||
return <div className="font-mono">-</div>
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
accessorKey: "createdAt",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div>
|
||||
{new Date(row.original.doc.createdAt).toLocaleString()}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export function DirectoryContentTable() {
|
||||
return (
|
||||
<DirectoryContentTableContextMenu>
|
||||
<div className="w-full">
|
||||
<DirectoryContentTableContent />
|
||||
</div>
|
||||
</DirectoryContentTableContextMenu>
|
||||
function useTableColumns(
|
||||
onOpenFile: (file: Doc<"files">) => void,
|
||||
directoryUrlFn: (directory: Doc<"directories">) => string,
|
||||
): ColumnDef<FileSystemItem>[] {
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => {
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onCheckedChange={row.getToggleSelectedHandler()}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 24,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "doc.name",
|
||||
cell: ({ row }) => {
|
||||
switch (row.original.kind) {
|
||||
case FileType.File:
|
||||
return (
|
||||
<FileNameCell
|
||||
file={row.original.doc}
|
||||
onOpenFile={onOpenFile}
|
||||
/>
|
||||
)
|
||||
case FileType.Directory:
|
||||
return (
|
||||
<DirectoryNameCell
|
||||
directory={row.original.doc}
|
||||
directoryUrlFn={directoryUrlFn}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
size: 1000,
|
||||
},
|
||||
{
|
||||
header: "Size",
|
||||
accessorKey: "size",
|
||||
cell: ({ row }) => {
|
||||
switch (row.original.kind) {
|
||||
case FileType.File:
|
||||
return (
|
||||
<div>
|
||||
{formatFileSize(row.original.doc.size)}
|
||||
</div>
|
||||
)
|
||||
case FileType.Directory:
|
||||
return <div className="font-mono">-</div>
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
accessorKey: "createdAt",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div>
|
||||
{new Date(
|
||||
row.original.doc.createdAt,
|
||||
).toLocaleString()}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
[onOpenFile, directoryUrlFn],
|
||||
)
|
||||
}
|
||||
|
||||
export function DirectoryContentTableContextMenu({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const store = useStore()
|
||||
const [target, setTarget] = useAtom(contextMenuTargeItemsAtom)
|
||||
const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom)
|
||||
const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash)
|
||||
const setItemBeingRenamed = useSetAtom(itemBeingRenamedAtom)
|
||||
const { mutate: moveToTrash } = useMutation({
|
||||
mutationFn: moveToTrashMutation,
|
||||
onMutate: ({ handles }) => {
|
||||
setOptimisticDeletedItems(
|
||||
(prev) =>
|
||||
new Set([...prev, ...handles.map((handle) => handle.id)]),
|
||||
)
|
||||
},
|
||||
onSuccess: ({ deleted, errors }, { handles }) => {
|
||||
setOptimisticDeletedItems((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
for (const handle of handles) {
|
||||
newSet.delete(handle.id)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
if (errors.length === 0 && deleted.length === handles.length) {
|
||||
toast.success(`Moved ${handles.length} items to trash`)
|
||||
} else if (errors.length === handles.length) {
|
||||
toast.error("Failed to move to trash")
|
||||
} else {
|
||||
toast.info(
|
||||
`Moved ${deleted.length} items to trash; failed to move ${errors.length} items`,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const handleRename = () => {
|
||||
const selectedItems = store.get(contextMenuTargeItemsAtom)
|
||||
if (selectedItems.length === 1) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: length is checked
|
||||
const selectedItem = selectedItems[0]!
|
||||
setItemBeingRenamed({
|
||||
originalItem: selectedItem,
|
||||
name: selectedItem.doc.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
const selectedItems = store.get(contextMenuTargeItemsAtom)
|
||||
if (selectedItems.length > 0) {
|
||||
moveToTrash({
|
||||
handles: selectedItems.map(newFileSystemHandle),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setTarget([])
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||
{target && (
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem onClick={handleRename}>
|
||||
<TextCursorInputIcon />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={handleDelete}>
|
||||
<TrashIcon />
|
||||
Move to trash
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export function DirectoryContentTableContent() {
|
||||
export function DirectoryContentTable({
|
||||
filterFn,
|
||||
directoryUrlFn,
|
||||
onContextMenu,
|
||||
fileDragInfoAtom,
|
||||
onOpenFile,
|
||||
}: DirectoryContentTableProps) {
|
||||
const { directoryContent } = useContext(DirectoryPageContext)
|
||||
const optimisticDeletedItems = useAtomValue(optimisticDeletedItemsAtom)
|
||||
const setContextMenuTargetItem = useSetAtom(contextMenuTargeItemsAtom)
|
||||
const store = useStore()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const table = useReactTable({
|
||||
data: directoryContent || [],
|
||||
columns,
|
||||
columns: useTableColumns(onOpenFile, directoryUrlFn),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
enableRowSelection: true,
|
||||
enableGlobalFilter: true,
|
||||
globalFilterFn: (row, _columnId, _filterValue, _addMeta) => {
|
||||
return !optimisticDeletedItems.has(row.original.doc._id)
|
||||
},
|
||||
globalFilterFn: (row, _columnId, _filterValue, _addMeta) =>
|
||||
filterFn(row.original),
|
||||
getRowId: (row) => row.doc._id,
|
||||
})
|
||||
|
||||
@@ -262,17 +188,10 @@ export function DirectoryContentTableContent() {
|
||||
row: Row<FileSystemItem>,
|
||||
_event: React.MouseEvent,
|
||||
) => {
|
||||
const target = store.get(contextMenuTargeItemsAtom)
|
||||
if (target.length > 0) {
|
||||
setContextMenuTargetItem([])
|
||||
} else if (row.getIsSelected()) {
|
||||
setContextMenuTargetItem(
|
||||
table.getSelectedRowModel().rows.map((row) => row.original),
|
||||
)
|
||||
} else {
|
||||
if (!row.getIsSelected()) {
|
||||
selectRow(row)
|
||||
setContextMenuTargetItem([row.original])
|
||||
}
|
||||
onContextMenu(row, table)
|
||||
}
|
||||
|
||||
const selectRow = (row: Row<FileSystemItem>) => {
|
||||
@@ -336,6 +255,7 @@ export function DirectoryContentTableContent() {
|
||||
table={table}
|
||||
row={row}
|
||||
onClick={() => selectRow(row)}
|
||||
fileDragInfoAtom={fileDragInfoAtom}
|
||||
onContextMenu={(e) =>
|
||||
handleRowContextMenu(row, e)
|
||||
}
|
||||
@@ -355,8 +275,8 @@ export function DirectoryContentTableContent() {
|
||||
|
||||
function NoResultsRow() {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="text-center">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={4} className="text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -369,22 +289,24 @@ function FileItemRow({
|
||||
onClick,
|
||||
onContextMenu,
|
||||
onDoubleClick,
|
||||
fileDragInfoAtom,
|
||||
}: {
|
||||
table: TableType<FileSystemItem>
|
||||
row: Row<FileSystemItem>
|
||||
onClick: () => void
|
||||
onContextMenu: (e: React.MouseEvent) => void
|
||||
onDoubleClick: () => void
|
||||
fileDragInfoAtom: PrimitiveAtom<FileDragInfo | null>
|
||||
}) {
|
||||
const ref = useRef<HTMLTableRowElement>(null)
|
||||
const setDragInfo = useSetAtom(dragInfoAtom)
|
||||
const setFileDragInfo = useSetAtom(fileDragInfoAtom)
|
||||
|
||||
const { isDraggedOver, dropHandlers } = useFileDrop({
|
||||
destItem:
|
||||
row.original.kind === FileType.Directory
|
||||
? newDirectoryHandle(row.original.doc._id)
|
||||
: null,
|
||||
dragInfoAtom,
|
||||
dragInfoAtom: fileDragInfoAtom,
|
||||
})
|
||||
|
||||
const handleDragStart = (_e: React.DragEvent) => {
|
||||
@@ -411,14 +333,14 @@ function FileItemRow({
|
||||
draggedItems = [source]
|
||||
}
|
||||
|
||||
setDragInfo({
|
||||
setFileDragInfo({
|
||||
source,
|
||||
items: draggedItems,
|
||||
})
|
||||
}
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setDragInfo(null)
|
||||
setFileDragInfo(null)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -448,23 +370,30 @@ function FileItemRow({
|
||||
)
|
||||
}
|
||||
|
||||
function DirectoryNameCell({ directory }: { directory: Doc<"directories"> }) {
|
||||
function DirectoryNameCell({
|
||||
directory,
|
||||
directoryUrlFn,
|
||||
}: {
|
||||
directory: Doc<"directories">
|
||||
directoryUrlFn: (directory: Doc<"directories">) => string
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<DirectoryIcon className="size-4" />
|
||||
<Link
|
||||
className="hover:underline"
|
||||
to={`/directories/${directory._id}`}
|
||||
>
|
||||
<Link className="hover:underline" to={directoryUrlFn(directory)}>
|
||||
{directory.name}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FileNameCell({ file }: { file: Doc<"files"> }) {
|
||||
const setOpenedFile = useSetAtom(openedFileAtom)
|
||||
|
||||
function FileNameCell({
|
||||
file,
|
||||
onOpenFile,
|
||||
}: {
|
||||
file: Doc<"files">
|
||||
onOpenFile: (file: Doc<"files">) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<TextFileIcon className="size-4" />
|
||||
@@ -472,7 +401,7 @@ function FileNameCell({ file }: { file: Doc<"files"> }) {
|
||||
type="button"
|
||||
className="hover:underline cursor-pointer"
|
||||
onClick={() => {
|
||||
setOpenedFile(file)
|
||||
onOpenFile(file)
|
||||
}}
|
||||
>
|
||||
{file.name}
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import {
|
||||
type DirectoryHandle,
|
||||
FileType,
|
||||
type PathComponent,
|
||||
} from "@fileone/convex/model/filesystem"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { Link } from "@tanstack/react-router"
|
||||
import { useMutation as useConvexMutation } from "convex/react"
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
Loader2Icon,
|
||||
PlusIcon,
|
||||
UploadCloudIcon,
|
||||
} from "lucide-react"
|
||||
import React, { type ChangeEvent, Fragment, useContext, useEffect, useRef } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { ImagePreviewDialog } from "@/components/image-preview-dialog"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { DirectoryIcon } from "../../components/icons/directory-icon"
|
||||
import { TextFileIcon } from "../../components/icons/text-file-icon"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "../../components/ui/breadcrumb"
|
||||
import { Button } from "../../components/ui/button"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "../../components/ui/tooltip"
|
||||
import { WithAtom } from "../../components/with-atom"
|
||||
import { useFileDrop } from "../../files/use-file-drop"
|
||||
import { cn } from "../../lib/utils"
|
||||
import { isControlOrCommandKeyActive, keyboardModifierAtom } from "../../lib/keyboard"
|
||||
import { DirectoryPageContext } from "./context"
|
||||
import { DirectoryContentTable } from "./directory-content-table"
|
||||
import { NewDirectoryDialog } from "./new-directory-dialog"
|
||||
import { RenameFileDialog } from "./rename-file-dialog"
|
||||
import { dragInfoAtom, newFileTypeAtom, openedFileAtom } from "./state"
|
||||
|
||||
export function DirectoryPage() {
|
||||
const { directory } = useContext(DirectoryPageContext)
|
||||
const setNewFileType = useSetAtom(newFileTypeAtom)
|
||||
const keyboardModifiers = useAtomValue(keyboardModifierAtom)
|
||||
|
||||
// Keyboard shortcut handler for Cmd/Ctrl+Shift+N to create new directory
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === "N" &&
|
||||
event.shiftKey &&
|
||||
isControlOrCommandKeyActive(keyboardModifiers)
|
||||
) {
|
||||
event.preventDefault()
|
||||
setNewFileType(FileType.Directory)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [keyboardModifiers, setNewFileType])
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="flex py-1 shrink-0 items-center gap-2 border-b px-4 w-full">
|
||||
<FilePathBreadcrumb />
|
||||
<div className="ml-auto flex flex-row gap-2">
|
||||
<NewDirectoryItemDropdown />
|
||||
<UploadFileButton />
|
||||
</div>
|
||||
</header>
|
||||
<div className="w-full">
|
||||
<DirectoryContentTable />
|
||||
</div>
|
||||
<RenameFileDialog />
|
||||
<PreviewDialog />
|
||||
<WithAtom atom={newFileTypeAtom}>
|
||||
{(newFileType, setNewFileType) => (
|
||||
<NewDirectoryDialog
|
||||
open={newFileType === FileType.Directory}
|
||||
directoryId={directory._id}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setNewFileType(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</WithAtom>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function FilePathBreadcrumb() {
|
||||
const { rootDirectory, directory } = useContext(DirectoryPageContext)
|
||||
|
||||
const breadcrumbItems: React.ReactNode[] = []
|
||||
for (let i = 1; i < directory.path.length - 1; i++) {
|
||||
breadcrumbItems.push(
|
||||
<Fragment key={directory.path[i]!.handle.id}>
|
||||
<BreadcrumbSeparator />
|
||||
<FilePathBreadcrumbItem component={directory.path[i]!} />
|
||||
</Fragment>,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{rootDirectory._id === directory._id ? (
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>All Files</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
) : (
|
||||
<FilePathBreadcrumbItem component={directory.path[0]!} />
|
||||
)}
|
||||
{breadcrumbItems}
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{directory.name}</BreadcrumbPage>{" "}
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
|
||||
function FilePathBreadcrumbItem({ component }: { component: PathComponent }) {
|
||||
const { isDraggedOver, dropHandlers } = useFileDrop({
|
||||
destItem: component.handle as DirectoryHandle,
|
||||
dragInfoAtom,
|
||||
})
|
||||
|
||||
const dirName = component.name || "All Files"
|
||||
|
||||
return (
|
||||
<Tooltip open={isDraggedOver}>
|
||||
<TooltipTrigger asChild>
|
||||
<BreadcrumbItem
|
||||
className={cn({ "bg-muted": isDraggedOver })}
|
||||
{...dropHandlers}
|
||||
>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={`/directories/${component.handle.id}`}>
|
||||
{dirName}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Move to {dirName}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
// tags: upload, uploadfile, uploadfilebutton, fileupload, fileuploadbutton
|
||||
function UploadFileButton() {
|
||||
const { directory } = useContext(DirectoryPageContext)
|
||||
const generateUploadUrl = useConvexMutation(api.files.generateUploadUrl)
|
||||
const saveFile = useConvexMutation(api.files.saveFile)
|
||||
const { mutate: uploadFile, isPending: isUploading } = useMutation({
|
||||
mutationFn: async (file: File) => {
|
||||
const uploadUrl = await generateUploadUrl()
|
||||
const uploadResult = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
body: file,
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
},
|
||||
})
|
||||
const { storageId } = await uploadResult.json()
|
||||
|
||||
await saveFile({
|
||||
storageId,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
mimeType: file.type,
|
||||
directoryId: directory._id,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("File uploaded successfully.")
|
||||
},
|
||||
})
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleClick = () => {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
const onFileUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) {
|
||||
uploadFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
hidden
|
||||
onChange={onFileUpload}
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
name="files"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{isUploading ? (
|
||||
<Loader2Icon className="animate-spin size-4" />
|
||||
) : (
|
||||
<UploadCloudIcon className="size-4" />
|
||||
)}
|
||||
{isUploading ? "Uploading" : "Upload File"}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function NewDirectoryItemDropdown() {
|
||||
const [newFileType, setNewFileType] = useAtom(newFileTypeAtom)
|
||||
|
||||
const addNewDirectory = () => {
|
||||
setNewFileType(FileType.Directory)
|
||||
}
|
||||
|
||||
const handleCloseAutoFocus = (event: Event) => {
|
||||
// If we just created a new item, prevent the dropdown from restoring focus to the trigger
|
||||
if (newFileType) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" type="button" variant="outline">
|
||||
<PlusIcon className="size-4" />
|
||||
New
|
||||
<ChevronDownIcon className="pl-1 size-4 shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent onCloseAutoFocus={handleCloseAutoFocus}>
|
||||
<DropdownMenuItem>
|
||||
<TextFileIcon />
|
||||
Text file
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={addNewDirectory}>
|
||||
<DirectoryIcon />
|
||||
Directory
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviewDialog() {
|
||||
const [openedFile, setOpenedFile] = useAtom(openedFileAtom)
|
||||
|
||||
if (!openedFile) return null
|
||||
|
||||
switch (openedFile.mimeType) {
|
||||
case "image/jpeg":
|
||||
case "image/png":
|
||||
case "image/gif":
|
||||
return (
|
||||
<ImagePreviewDialog
|
||||
file={openedFile}
|
||||
onClose={() => setOpenedFile(null)}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { Id } from "@fileone/convex/_generated/dataModel"
|
||||
import type {
|
||||
DirectoryHandle,
|
||||
DirectoryPathComponent,
|
||||
} from "@fileone/convex/model/filesystem"
|
||||
import { Link } from "@tanstack/react-router"
|
||||
import { Fragment, useContext } from "react"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "../../components/ui/breadcrumb"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "../../components/ui/tooltip"
|
||||
import { useFileDrop } from "../../files/use-file-drop"
|
||||
import { cn } from "../../lib/utils"
|
||||
import { DirectoryPageContext } from "./context"
|
||||
import { dragInfoAtom } from "./state"
|
||||
|
||||
export function FilePathBreadcrumb({
|
||||
rootLabel,
|
||||
directoryUrlFn,
|
||||
}: {
|
||||
rootLabel: string
|
||||
directoryUrlFn: (directory: Id<"directories">) => string
|
||||
}) {
|
||||
const { rootDirectory, directory } = useContext(DirectoryPageContext)
|
||||
|
||||
const breadcrumbItems: React.ReactNode[] = []
|
||||
for (let i = 1; i < directory.path.length - 1; i++) {
|
||||
breadcrumbItems.push(
|
||||
<Fragment key={directory.path[i]?.handle.id}>
|
||||
<BreadcrumbSeparator />
|
||||
<FilePathBreadcrumbItem
|
||||
component={directory.path[i]!}
|
||||
rootLabel={rootLabel}
|
||||
directoryUrlFn={directoryUrlFn}
|
||||
/>
|
||||
</Fragment>,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{rootDirectory._id === directory._id ? (
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{rootLabel}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
) : (
|
||||
<FilePathBreadcrumbItem
|
||||
component={directory.path[0]!}
|
||||
rootLabel={rootLabel}
|
||||
directoryUrlFn={directoryUrlFn}
|
||||
/>
|
||||
)}
|
||||
{breadcrumbItems}
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{directory.name}</BreadcrumbPage>{" "}
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
|
||||
function FilePathBreadcrumbItem({
|
||||
component,
|
||||
rootLabel,
|
||||
directoryUrlFn,
|
||||
}: {
|
||||
component: DirectoryPathComponent
|
||||
rootLabel: string
|
||||
directoryUrlFn: (directory: Id<"directories">) => string
|
||||
}) {
|
||||
const { isDraggedOver, dropHandlers } = useFileDrop({
|
||||
destItem: component.handle as DirectoryHandle,
|
||||
dragInfoAtom,
|
||||
})
|
||||
|
||||
const dirName = component.name || rootLabel
|
||||
|
||||
return (
|
||||
<Tooltip open={isDraggedOver}>
|
||||
<TooltipTrigger asChild>
|
||||
<BreadcrumbItem
|
||||
className={cn({ "bg-muted": isDraggedOver })}
|
||||
{...dropHandlers}
|
||||
>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={directoryUrlFn(component.handle.id)}>
|
||||
{dirName}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Move to {dirName}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import { type FileSystemItem, FileType } from "@fileone/convex/model/filesystem"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useMutation as useContextMutation } from "convex/react"
|
||||
import { atom, useAtom, useStore } from "jotai"
|
||||
import { useId } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -14,66 +13,56 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { itemBeingRenamedAtom } from "./state"
|
||||
|
||||
const fielNameAtom = atom(
|
||||
(get) => get(itemBeingRenamedAtom)?.name,
|
||||
(get, set, newName: string) => {
|
||||
const current = get(itemBeingRenamedAtom)
|
||||
if (current) {
|
||||
set(itemBeingRenamedAtom, {
|
||||
...current,
|
||||
name: newName,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
type RenameFileDialogProps = {
|
||||
item: FileSystemItem
|
||||
onRenameSuccess: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function RenameFileDialog() {
|
||||
const [itemBeingRenamed, setItemBeingRenamed] =
|
||||
useAtom(itemBeingRenamedAtom)
|
||||
const store = useStore()
|
||||
export function RenameFileDialog({
|
||||
item,
|
||||
onRenameSuccess,
|
||||
onClose,
|
||||
}: RenameFileDialogProps) {
|
||||
const formId = useId()
|
||||
|
||||
const { mutate: renameFile, isPending: isRenaming } = useMutation({
|
||||
mutationFn: useContextMutation(api.files.renameFile),
|
||||
onSuccess: () => {
|
||||
setItemBeingRenamed(null)
|
||||
toast.success("File renamed successfully")
|
||||
onRenameSuccess()
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
|
||||
const itemBeingRenamed = store.get(itemBeingRenamedAtom)
|
||||
if (itemBeingRenamed) {
|
||||
const formData = new FormData(event.currentTarget)
|
||||
const newName = formData.get("itemName") as string
|
||||
const formData = new FormData(event.currentTarget)
|
||||
const newName = formData.get("itemName") as string
|
||||
|
||||
if (newName) {
|
||||
switch (itemBeingRenamed.originalItem.kind) {
|
||||
case "file":
|
||||
renameFile({
|
||||
directoryId:
|
||||
itemBeingRenamed.originalItem.doc.directoryId,
|
||||
itemId: itemBeingRenamed.originalItem.doc._id,
|
||||
newName,
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
if (newName) {
|
||||
switch (item.kind) {
|
||||
case FileType.File:
|
||||
renameFile({
|
||||
directoryId: item.doc.directoryId,
|
||||
itemId: item.doc._id,
|
||||
newName,
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={itemBeingRenamed !== null}
|
||||
onOpenChange={(open) =>
|
||||
setItemBeingRenamed(open ? itemBeingRenamed : null)
|
||||
}
|
||||
open
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
onClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
@@ -81,7 +70,7 @@ export function RenameFileDialog() {
|
||||
</DialogHeader>
|
||||
|
||||
<form id={formId} onSubmit={onSubmit}>
|
||||
<RenameFileInput />
|
||||
<RenameFileInput initialValue={item.doc.name} />
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
@@ -99,13 +88,6 @@ export function RenameFileDialog() {
|
||||
)
|
||||
}
|
||||
|
||||
function RenameFileInput() {
|
||||
const [fileName, setFileName] = useAtom(fielNameAtom)
|
||||
return (
|
||||
<Input
|
||||
value={fileName}
|
||||
name="itemName"
|
||||
onChange={(e) => setFileName(e.target.value)}
|
||||
/>
|
||||
)
|
||||
function RenameFileInput({ initialValue }: { initialValue: string }) {
|
||||
return <Input defaultValue={initialValue} name="itemName" />
|
||||
}
|
||||
|
||||
@@ -10,33 +10,37 @@ export function SkeletonDemo() {
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
<Button
|
||||
onClick={() => setShowPageSkeleton(!showPageSkeleton)}
|
||||
variant={showPageSkeleton ? "default" : "outline"}
|
||||
>
|
||||
{showPageSkeleton ? "Hide" : "Show"} Page Skeleton
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
onClick={() => setShowTableSkeleton(!showTableSkeleton)}
|
||||
variant={showTableSkeleton ? "default" : "outline"}
|
||||
>
|
||||
{showTableSkeleton ? "Hide" : "Show"} Table Skeleton
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{showPageSkeleton && (
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">Directory Page Skeleton</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Directory Page Skeleton
|
||||
</h3>
|
||||
<DirectoryPageSkeleton />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{showTableSkeleton && (
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">Directory Content Table Skeleton</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Directory Content Table Skeleton
|
||||
</h3>
|
||||
<DirectoryContentTableSkeleton rows={5} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
|
||||
import type { DirectoryItemKind } from "@fileone/convex/model/directories"
|
||||
import type { FileSystemItem, FileType } from "@fileone/convex/model/filesystem"
|
||||
import type { RowSelectionState } from "@tanstack/react-table"
|
||||
import { atom } from "jotai"
|
||||
@@ -12,8 +11,6 @@ export const optimisticDeletedItemsAtom = atom(
|
||||
|
||||
export const selectedFileRowsAtom = atom<RowSelectionState>({})
|
||||
|
||||
export const newFileTypeAtom = atom<FileType | null>(null)
|
||||
|
||||
export const itemBeingRenamedAtom = atom<{
|
||||
originalItem: FileSystemItem
|
||||
name: string
|
||||
|
||||
71
packages/web/src/files/PickedFileItem.tsx
Normal file
71
packages/web/src/files/PickedFileItem.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useAtomValue } from "jotai"
|
||||
import { CircleAlertIcon, XIcon } from "lucide-react"
|
||||
import type React from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Tooltip } from "@/components/ui/tooltip"
|
||||
import { FileUploadStatusKind, fileUploadStatusAtomFamily } from "./store"
|
||||
import type { PickedFile } from "./upload-file-dialog"
|
||||
|
||||
export function PickedFileItem({
|
||||
file: pickedFile,
|
||||
onRemove,
|
||||
}: {
|
||||
file: PickedFile
|
||||
onRemove: (file: PickedFile) => void
|
||||
}) {
|
||||
const fileUploadAtom = fileUploadStatusAtomFamily(pickedFile.id)
|
||||
const fileUpload = useAtomValue(fileUploadAtom)
|
||||
console.log("fileUpload", fileUpload)
|
||||
const { file, id } = pickedFile
|
||||
|
||||
let statusIndicator: React.ReactNode
|
||||
if (!fileUpload) {
|
||||
statusIndicator = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onRemove(pickedFile)}
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
)
|
||||
} else {
|
||||
switch (fileUpload.kind) {
|
||||
case FileUploadStatusKind.InProgress:
|
||||
statusIndicator = <Progress value={fileUpload.progress * 100} />
|
||||
break
|
||||
case FileUploadStatusKind.Error:
|
||||
statusIndicator = (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<CircleAlertIcon />
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className="pl-3 pr-1 py-0.5 h-8 hover:bg-muted flex justify-between items-center"
|
||||
key={id}
|
||||
>
|
||||
<span>{file.name}</span>
|
||||
{fileUpload ? (
|
||||
<Progress
|
||||
className="max-w-20"
|
||||
value={fileUpload.progress * 100}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onRemove(pickedFile)}
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
21
packages/web/src/files/file-preview-dialog.tsx
Normal file
21
packages/web/src/files/file-preview-dialog.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Doc } from "@fileone/convex/_generated/dataModel"
|
||||
import { ImagePreviewDialog } from "./image-preview-dialog"
|
||||
|
||||
export function FilePreviewDialog({
|
||||
file,
|
||||
onClose,
|
||||
}: {
|
||||
file: Doc<"files">
|
||||
onClose: () => void
|
||||
}) {
|
||||
if (!file) return null
|
||||
|
||||
switch (file.mimeType) {
|
||||
case "image/jpeg":
|
||||
case "image/png":
|
||||
case "image/gif":
|
||||
return <ImagePreviewDialog file={file} onClose={onClose} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,15 @@ import {
|
||||
ZoomOutIcon,
|
||||
} from "lucide-react"
|
||||
import { useEffect, useRef } from "react"
|
||||
import { Button } from "./ui/button"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
} from "./ui/dialog"
|
||||
import { LoadingSpinner } from "./ui/loading-spinner"
|
||||
} from "@/components/ui/dialog"
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner"
|
||||
|
||||
const zoomLevelAtom = atom(
|
||||
1,
|
||||
@@ -41,8 +41,8 @@ export function ImagePreviewDialog({
|
||||
file: Doc<"files">
|
||||
onClose: () => void
|
||||
}) {
|
||||
const fileUrl = useConvexQuery(api.files.generateFileUrl, {
|
||||
storageId: file.storageId,
|
||||
const fileUrl = useConvexQuery(api.filesystem.fetchFileUrl, {
|
||||
fileId: file._id,
|
||||
})
|
||||
const setZoomLevel = useSetAtom(zoomLevelAtom)
|
||||
|
||||
97
packages/web/src/files/store.ts
Normal file
97
packages/web/src/files/store.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { atom } from "jotai"
|
||||
import { atomFamily } from "jotai/utils"
|
||||
|
||||
export enum FileUploadStatusKind {
|
||||
InProgress = "InProgress",
|
||||
Error = "Error",
|
||||
Success = "Success",
|
||||
}
|
||||
|
||||
export type FileUploadInProgress = {
|
||||
kind: FileUploadStatusKind.InProgress
|
||||
progress: number
|
||||
}
|
||||
|
||||
export type FileUploadError = {
|
||||
kind: FileUploadStatusKind.Error
|
||||
error: unknown
|
||||
}
|
||||
|
||||
export type FileUploadSuccess = {
|
||||
kind: FileUploadStatusKind.Success
|
||||
}
|
||||
|
||||
export type FileUploadStatus =
|
||||
| FileUploadInProgress
|
||||
| FileUploadError
|
||||
| FileUploadSuccess
|
||||
|
||||
export const fileUploadStatusesAtom = atom<Record<string, FileUploadStatus>>({})
|
||||
|
||||
export const fileUploadStatusAtomFamily = atomFamily((id: string) =>
|
||||
atom(
|
||||
(get) => get(fileUploadStatusesAtom)[id],
|
||||
(get, set, status: FileUploadStatus) => {
|
||||
const fileUploads = { ...get(fileUploadStatusesAtom) }
|
||||
fileUploads[id] = status
|
||||
set(fileUploadStatusesAtom, fileUploads)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
export const clearFileUploadStatusesAtom = atom(
|
||||
null,
|
||||
(get, set, ids: string[]) => {
|
||||
const fileUploads = { ...get(fileUploadStatusesAtom) }
|
||||
for (const id of ids) {
|
||||
if (fileUploads[id]) {
|
||||
delete fileUploads[id]
|
||||
}
|
||||
fileUploadStatusAtomFamily.remove(id)
|
||||
}
|
||||
set(fileUploadStatusesAtom, fileUploads)
|
||||
},
|
||||
)
|
||||
|
||||
export const clearAllFileUploadStatusesAtom = atom(
|
||||
null,
|
||||
(get, set) => {
|
||||
set(fileUploadStatusesAtom, {})
|
||||
},
|
||||
)
|
||||
|
||||
export const fileUploadCountAtom = atom(
|
||||
(get) => Object.keys(get(fileUploadStatusesAtom)).length,
|
||||
)
|
||||
|
||||
export const inProgressFileUploadCountAtom = atom((get) => {
|
||||
const statuses = get(fileUploadStatusesAtom)
|
||||
let count = 0
|
||||
for (const status in statuses) {
|
||||
if (statuses[status]?.kind === FileUploadStatusKind.InProgress) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
})
|
||||
|
||||
export const successfulFileUploadCountAtom = atom((get) => {
|
||||
const statuses = get(fileUploadStatusesAtom)
|
||||
let count = 0
|
||||
for (const status in statuses) {
|
||||
if (statuses[status]?.kind === FileUploadStatusKind.Success) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
})
|
||||
|
||||
export const hasFileUploadsErrorAtom = atom((get) => {
|
||||
const statuses = get(fileUploadStatusesAtom)
|
||||
for (const status in statuses) {
|
||||
if (statuses[status]?.kind === FileUploadStatusKind.Error) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
599
packages/web/src/files/upload-file-dialog.tsx
Normal file
599
packages/web/src/files/upload-file-dialog.tsx
Normal file
@@ -0,0 +1,599 @@
|
||||
import type { Doc } from "@fileone/convex/_generated/dataModel"
|
||||
import { mutationOptions } from "@tanstack/react-query"
|
||||
import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
||||
import { atomEffect } from "jotai-effect"
|
||||
import { atomWithMutation } from "jotai-tanstack-query"
|
||||
import {
|
||||
CircleAlertIcon,
|
||||
CircleCheckIcon,
|
||||
FilePlus2Icon,
|
||||
UploadCloudIcon,
|
||||
XIcon,
|
||||
} from "lucide-react"
|
||||
import { nanoid } from "nanoid"
|
||||
import type React from "react"
|
||||
import { useId, useMemo, useRef, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { formatError } from "@/lib/error"
|
||||
import {
|
||||
clearAllFileUploadStatusesAtom,
|
||||
clearFileUploadStatusesAtom,
|
||||
FileUploadStatusKind,
|
||||
fileUploadCountAtom,
|
||||
fileUploadStatusAtomFamily,
|
||||
fileUploadStatusesAtom,
|
||||
hasFileUploadsErrorAtom,
|
||||
successfulFileUploadCountAtom,
|
||||
} from "./store"
|
||||
import useUploadFile from "./use-upload-file"
|
||||
|
||||
type UploadFileDialogProps = {
|
||||
targetDirectory: Doc<"directories">
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
// Upload file atoms
|
||||
export type PickedFile = {
|
||||
id: string
|
||||
file: File
|
||||
}
|
||||
|
||||
export const pickedFilesAtom = atom<PickedFile[]>([])
|
||||
|
||||
function useUploadFilesAtom({
|
||||
targetDirectory,
|
||||
}: {
|
||||
targetDirectory: Doc<"directories">
|
||||
}) {
|
||||
const uploadFile = useUploadFile({ targetDirectory })
|
||||
const store = useStore()
|
||||
const options = useMemo(
|
||||
() =>
|
||||
mutationOptions({
|
||||
mutationFn: async (files: PickedFile[]) => {
|
||||
const promises = files.map((pickedFile) =>
|
||||
uploadFile({
|
||||
file: pickedFile.file,
|
||||
onStart: () => {
|
||||
store.set(
|
||||
fileUploadStatusAtomFamily(pickedFile.id),
|
||||
{
|
||||
kind: FileUploadStatusKind.InProgress,
|
||||
progress: 0,
|
||||
},
|
||||
)
|
||||
},
|
||||
onProgress: (progress) => {
|
||||
store.set(
|
||||
fileUploadStatusAtomFamily(pickedFile.id),
|
||||
{
|
||||
kind: FileUploadStatusKind.InProgress,
|
||||
progress,
|
||||
},
|
||||
)
|
||||
},
|
||||
}).catch((error) => {
|
||||
console.log("error", error)
|
||||
store.set(
|
||||
fileUploadStatusAtomFamily(pickedFile.id),
|
||||
{
|
||||
kind: FileUploadStatusKind.Error,
|
||||
error,
|
||||
},
|
||||
)
|
||||
throw error
|
||||
}),
|
||||
)
|
||||
return await Promise.allSettled(promises)
|
||||
},
|
||||
onSuccess: (results, files) => {
|
||||
const remainingPickedFiles: PickedFile[] = []
|
||||
results.forEach((result, i) => {
|
||||
// biome-ignore lint/style/noNonNullAssertion: results lenght must match input files array length
|
||||
const pickedFile = files[i]!
|
||||
const statusAtom = fileUploadStatusAtomFamily(
|
||||
pickedFile.id,
|
||||
)
|
||||
switch (result.status) {
|
||||
case "fulfilled":
|
||||
store.set(statusAtom, {
|
||||
kind: FileUploadStatusKind.Success,
|
||||
})
|
||||
break
|
||||
case "rejected":
|
||||
store.set(statusAtom, {
|
||||
kind: FileUploadStatusKind.Error,
|
||||
error: result.reason,
|
||||
})
|
||||
remainingPickedFiles.push(pickedFile)
|
||||
break
|
||||
}
|
||||
})
|
||||
// setPickedFiles(remainingPickedFiles)
|
||||
|
||||
if (remainingPickedFiles.length === 0) {
|
||||
toast.success("All files uploaded successfully")
|
||||
}
|
||||
},
|
||||
}),
|
||||
[uploadFile, store.set],
|
||||
)
|
||||
return useMemo(() => atomWithMutation(() => options), [options])
|
||||
}
|
||||
type UploadFilesAtom = ReturnType<typeof useUploadFilesAtom>
|
||||
|
||||
export function UploadFileDialog({
|
||||
targetDirectory,
|
||||
onClose,
|
||||
}: UploadFileDialogProps) {
|
||||
const formId = useId()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const setPickedFiles = useSetAtom(pickedFilesAtom)
|
||||
const clearFileUploadStatuses = useSetAtom(clearFileUploadStatusesAtom)
|
||||
const store = useStore()
|
||||
|
||||
const updateFileInputEffect = useMemo(
|
||||
() =>
|
||||
atomEffect((get) => {
|
||||
const dataTransfer = new DataTransfer()
|
||||
const pickedFiles = get(pickedFilesAtom)
|
||||
for (const { file } of pickedFiles) {
|
||||
dataTransfer.items.add(file)
|
||||
}
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.files = dataTransfer.files
|
||||
}
|
||||
}),
|
||||
[],
|
||||
)
|
||||
useAtom(updateFileInputEffect)
|
||||
|
||||
const uploadFilesAtom = useUploadFilesAtom({
|
||||
targetDirectory,
|
||||
})
|
||||
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function openFilePicker() {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const files = event.target.files
|
||||
if (files) {
|
||||
setPickedFiles((prev) => [
|
||||
...prev,
|
||||
...Array.from(files).map((file) => ({ id: nanoid(), file })),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
function onUploadButtonClick() {
|
||||
const uploadStatuses = store.get(fileUploadStatusesAtom)
|
||||
const fileUploadCount = store.get(fileUploadCountAtom)
|
||||
const pickedFiles = store.get(pickedFilesAtom)
|
||||
const { mutate: uploadFiles, reset: restUploadFilesMutation } =
|
||||
store.get(uploadFilesAtom)
|
||||
|
||||
if (pickedFiles.length === 0) {
|
||||
// no files are picked, nothing to upload
|
||||
return
|
||||
}
|
||||
|
||||
if (fileUploadCount === 0) {
|
||||
// no files are being uploaded, upload all picked files
|
||||
uploadFiles(pickedFiles)
|
||||
return
|
||||
}
|
||||
|
||||
const successfulUploads: PickedFile["id"][] = []
|
||||
const nextPickedFiles: PickedFile[] = []
|
||||
for (const file of pickedFiles) {
|
||||
const uploadStatus = uploadStatuses[file.id]
|
||||
if (uploadStatus) {
|
||||
switch (uploadStatus.kind) {
|
||||
case FileUploadStatusKind.Success:
|
||||
successfulUploads.push(file.id)
|
||||
continue
|
||||
case FileUploadStatusKind.InProgress:
|
||||
continue
|
||||
case FileUploadStatusKind.Error:
|
||||
nextPickedFiles.push(file)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearFileUploadStatuses(successfulUploads)
|
||||
|
||||
if (successfulUploads.length === pickedFiles.length) {
|
||||
// all files were successfully uploaded, close the dialog
|
||||
onClose()
|
||||
} else {
|
||||
// some files were not successfully uploaded, set the next picked files
|
||||
setPickedFiles(nextPickedFiles)
|
||||
restUploadFilesMutation()
|
||||
uploadFiles(nextPickedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose()
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<UploadDialogHeader
|
||||
uploadFilesAtom={uploadFilesAtom}
|
||||
targetDirectory={targetDirectory}
|
||||
/>
|
||||
|
||||
<form id={formId} onSubmit={handleSubmit}>
|
||||
<input
|
||||
hidden
|
||||
multiple
|
||||
type="file"
|
||||
name="files"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
<UploadFileDropContainer>
|
||||
<UploadFileArea onClick={openFilePicker} />
|
||||
</UploadFileDropContainer>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<ContinueUploadAfterSuccessfulUploadButton
|
||||
uploadFilesAtom={uploadFilesAtom}
|
||||
/>
|
||||
<SelectMoreFilesButton
|
||||
onClick={openFilePicker}
|
||||
uploadFilesAtom={uploadFilesAtom}
|
||||
/>
|
||||
<UploadButton
|
||||
uploadFilesAtom={uploadFilesAtom}
|
||||
onClick={onUploadButtonClick}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function UploadDialogHeader({
|
||||
uploadFilesAtom,
|
||||
targetDirectory,
|
||||
}: {
|
||||
uploadFilesAtom: UploadFilesAtom
|
||||
targetDirectory: Doc<"directories">
|
||||
}) {
|
||||
const { data: uploadResults, isPending: isUploading } =
|
||||
useAtomValue(uploadFilesAtom)
|
||||
const successfulUploadCount = useAtomValue(successfulFileUploadCountAtom)
|
||||
|
||||
let dialogTitle: string
|
||||
let dialogDescription: string
|
||||
if (isUploading) {
|
||||
dialogTitle = "Uploading files"
|
||||
dialogDescription =
|
||||
"You can close the dialog while they are being uploaded in the background."
|
||||
} else if (
|
||||
uploadResults &&
|
||||
uploadResults.length > 0 &&
|
||||
successfulUploadCount === uploadResults.length
|
||||
) {
|
||||
dialogTitle = "Files uploaded"
|
||||
dialogDescription =
|
||||
"Click 'Done' to close the dialog, or select more files to upload."
|
||||
} else if (targetDirectory.name) {
|
||||
dialogTitle = `Upload file to "${targetDirectory.name}"`
|
||||
dialogDescription = "Drag and drop files here or click to select files"
|
||||
} else {
|
||||
dialogTitle = "Upload file"
|
||||
dialogDescription = "Drag and drop files here or click to select files"
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogHeader>
|
||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
||||
<DialogDescription>{dialogDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
)
|
||||
}
|
||||
|
||||
function ContinueUploadAfterSuccessfulUploadButton({
|
||||
uploadFilesAtom,
|
||||
}: {
|
||||
uploadFilesAtom: UploadFilesAtom
|
||||
}) {
|
||||
const setPickedFiles = useSetAtom(pickedFilesAtom)
|
||||
const clearAllFileUploadStatuses = useSetAtom(
|
||||
clearAllFileUploadStatusesAtom,
|
||||
)
|
||||
const {
|
||||
data: uploadResults,
|
||||
isPending: isUploading,
|
||||
reset: resetUploadFilesMutation,
|
||||
} = useAtomValue(uploadFilesAtom)
|
||||
const successfulUploadCount = useAtomValue(successfulFileUploadCountAtom)
|
||||
|
||||
if (
|
||||
!uploadResults ||
|
||||
uploadResults.length === 0 ||
|
||||
successfulUploadCount !== uploadResults.length
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
function resetUploadState() {
|
||||
setPickedFiles([])
|
||||
clearAllFileUploadStatuses()
|
||||
resetUploadFilesMutation()
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={resetUploadState}
|
||||
disabled={isUploading}
|
||||
>
|
||||
Upload more files
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* allows the user to select more files after they have selected some files for upload. only visible before any upload has been started.
|
||||
*/
|
||||
function SelectMoreFilesButton({
|
||||
onClick,
|
||||
uploadFilesAtom,
|
||||
}: {
|
||||
onClick: () => void
|
||||
uploadFilesAtom: UploadFilesAtom
|
||||
}) {
|
||||
const pickedFiles = useAtomValue(pickedFilesAtom)
|
||||
const { data: uploadResults, isPending: isUploading } =
|
||||
useAtomValue(uploadFilesAtom)
|
||||
|
||||
if (pickedFiles.length === 0 || uploadResults) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" onClick={onClick} disabled={isUploading}>
|
||||
Select more files
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function UploadButton({
|
||||
uploadFilesAtom,
|
||||
onClick,
|
||||
}: {
|
||||
uploadFilesAtom: UploadFilesAtom
|
||||
onClick: () => void
|
||||
}) {
|
||||
const pickedFiles = useAtomValue(pickedFilesAtom)
|
||||
const hasUploadErrors = useAtomValue(hasFileUploadsErrorAtom)
|
||||
const fileUploadCount = useAtomValue(fileUploadCountAtom)
|
||||
const { isPending: isUploading } = useAtomValue(uploadFilesAtom)
|
||||
|
||||
let label: string
|
||||
if (hasUploadErrors) {
|
||||
label = "Retry failed uploads"
|
||||
} else if (pickedFiles.length > 0) {
|
||||
if (fileUploadCount > 0) {
|
||||
label = "Done"
|
||||
} else {
|
||||
label = `Upload ${pickedFiles.length} files`
|
||||
}
|
||||
} else {
|
||||
label = "Upload"
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={onClick} disabled={isUploading} loading={isUploading}>
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function UploadFileDropContainer({ children }: React.PropsWithChildren) {
|
||||
const [draggedFiles, setDraggedFiles] = useState<DataTransferItem[]>([])
|
||||
const setPickedFiles = useSetAtom(pickedFilesAtom)
|
||||
|
||||
function handleDragOver(e: React.DragEvent) {
|
||||
e.preventDefault()
|
||||
const items = Array.from(e.dataTransfer.items)
|
||||
const draggedFiles = []
|
||||
for (const item of items) {
|
||||
if (item.kind === "file") {
|
||||
draggedFiles.push(item)
|
||||
}
|
||||
}
|
||||
setDraggedFiles(draggedFiles)
|
||||
}
|
||||
|
||||
function handleDragLeave() {
|
||||
setDraggedFiles([])
|
||||
}
|
||||
|
||||
function handleDrop(e: React.DragEvent) {
|
||||
e.preventDefault()
|
||||
const items = Array.from(e.dataTransfer.items)
|
||||
const droppedFiles: PickedFile[] = []
|
||||
for (const item of items) {
|
||||
const file = item.getAsFile()
|
||||
if (file) {
|
||||
droppedFiles.push({
|
||||
id: nanoid(),
|
||||
file,
|
||||
})
|
||||
}
|
||||
}
|
||||
setPickedFiles((prev) => [...prev, ...droppedFiles])
|
||||
setDraggedFiles([])
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
aria-label="File drop area"
|
||||
className="relative"
|
||||
>
|
||||
{children}
|
||||
{draggedFiles.length > 0 ? (
|
||||
<div className="border border-accent bg-primary text-primary-foreground absolute inset-0 rounded flex flex-col items-center justify-center text-sm space-y-1">
|
||||
<FilePlus2Icon className="animate-bounce" />
|
||||
<p>Drop {draggedFiles.length} files here</p>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// tag: uploadfilearea area fileuploadarea
|
||||
function UploadFileArea({ onClick }: { onClick: () => void }) {
|
||||
const [pickedFiles, setPickedFiles] = useAtom(pickedFilesAtom)
|
||||
|
||||
function removeSelectedFile(file: PickedFile) {
|
||||
setPickedFiles((prev) => prev.filter((f) => f.id !== file.id))
|
||||
}
|
||||
|
||||
if (pickedFiles.length > 0) {
|
||||
return (
|
||||
<PickedFilesList
|
||||
pickedFiles={pickedFiles}
|
||||
onRemoveFile={removeSelectedFile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full h-48 border-2 rounded border-dashed border-border flex flex-col items-center justify-center text-muted-foreground text-sm space-y-1 hover:bg-muted transition-all hover:border-solid"
|
||||
onClick={onClick}
|
||||
>
|
||||
<UploadCloudIcon />
|
||||
<span>Click to select files or drag and drop them here</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function PickedFilesList({
|
||||
pickedFiles,
|
||||
onRemoveFile,
|
||||
}: {
|
||||
pickedFiles: PickedFile[]
|
||||
onRemoveFile: (file: PickedFile) => void
|
||||
}) {
|
||||
return (
|
||||
<ul className="min-h-48 border border-border rounded bg-card text-sm">
|
||||
{pickedFiles.map((file: PickedFile) => (
|
||||
<PickedFileItem
|
||||
key={file.id}
|
||||
file={file}
|
||||
onRemove={onRemoveFile}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
function PickedFileItem({
|
||||
file: pickedFile,
|
||||
onRemove,
|
||||
}: {
|
||||
file: PickedFile
|
||||
onRemove: (file: PickedFile) => void
|
||||
}) {
|
||||
const fileUploadAtom = fileUploadStatusAtomFamily(pickedFile.id)
|
||||
const fileUpload = useAtomValue(fileUploadAtom)
|
||||
console.log("fileUpload", fileUpload)
|
||||
const { file, id } = pickedFile
|
||||
|
||||
let statusIndicator: React.ReactNode
|
||||
if (!fileUpload) {
|
||||
statusIndicator = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onRemove(pickedFile)}
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
)
|
||||
} else {
|
||||
switch (fileUpload.kind) {
|
||||
case FileUploadStatusKind.InProgress:
|
||||
statusIndicator = (
|
||||
<Progress
|
||||
className="max-w-20"
|
||||
value={fileUpload.progress * 100}
|
||||
/>
|
||||
)
|
||||
break
|
||||
case FileUploadStatusKind.Error:
|
||||
statusIndicator = (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<CircleAlertIcon className="pr-2 text-destructive" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Failed to upload file:{" "}
|
||||
{formatError(fileUpload.error)}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
break
|
||||
case FileUploadStatusKind.Success:
|
||||
statusIndicator = (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<CircleCheckIcon className="pr-2 text-green-500" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>File uploaded</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className="pl-3 pr-1 py-0.5 h-8 hover:bg-muted flex justify-between items-center border-b border-border"
|
||||
key={id}
|
||||
>
|
||||
<p>{file.name} </p>
|
||||
{statusIndicator}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
57
packages/web/src/files/use-upload-file.ts
Normal file
57
packages/web/src/files/use-upload-file.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
|
||||
import { useMutation as useConvexMutation } from "convex/react"
|
||||
import { useCallback } from "react"
|
||||
|
||||
function useUploadFile({
|
||||
targetDirectory,
|
||||
}: {
|
||||
targetDirectory: Doc<"directories">
|
||||
}) {
|
||||
const generateUploadUrl = useConvexMutation(api.files.generateUploadUrl)
|
||||
const saveFile = useConvexMutation(api.files.saveFile)
|
||||
|
||||
async function upload({
|
||||
file,
|
||||
onStart,
|
||||
onProgress,
|
||||
}: {
|
||||
file: File
|
||||
onStart: (xhr: XMLHttpRequest) => void
|
||||
onProgress: (progress: number) => void
|
||||
}) {
|
||||
const uploadUrl = await generateUploadUrl()
|
||||
|
||||
return new Promise<{ storageId: Id<"_storage"> }>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
onProgress(e.loaded / e.total)
|
||||
})
|
||||
xhr.upload.addEventListener("error", reject)
|
||||
xhr.addEventListener("load", () => {
|
||||
resolve(
|
||||
xhr.response as {
|
||||
storageId: Id<"_storage">
|
||||
},
|
||||
)
|
||||
})
|
||||
xhr.open("POST", uploadUrl)
|
||||
xhr.responseType = "json"
|
||||
xhr.setRequestHeader("Content-Type", file.type)
|
||||
xhr.send(file)
|
||||
onStart(xhr)
|
||||
}).then(({ storageId }) =>
|
||||
saveFile({
|
||||
storageId,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
mimeType: file.type,
|
||||
directoryId: targetDirectory._id,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return useCallback(upload, [])
|
||||
}
|
||||
|
||||
export default useUploadFile
|
||||
@@ -3,17 +3,19 @@ import * as React from "react"
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
return !!isMobile
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as SignUpRouteImport } from './routes/sign-up'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
|
||||
import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
|
||||
@@ -16,7 +17,13 @@ import { Route as LoginCallbackRouteImport } from './routes/login_.callback'
|
||||
import { Route as AuthenticatedSidebarLayoutRouteImport } from './routes/_authenticated/_sidebar-layout'
|
||||
import { Route as AuthenticatedSidebarLayoutHomeRouteImport } from './routes/_authenticated/_sidebar-layout/home'
|
||||
import { Route as AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport } from './routes/_authenticated/_sidebar-layout/directories.$directoryId'
|
||||
import { Route as AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRouteImport } from './routes/_authenticated/_sidebar-layout/trash.directories.$directoryId'
|
||||
|
||||
const SignUpRoute = SignUpRouteImport.update({
|
||||
id: '/sign-up',
|
||||
path: '/sign-up',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
id: '/login',
|
||||
path: '/login',
|
||||
@@ -53,60 +60,91 @@ const AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute =
|
||||
path: '/directories/$directoryId',
|
||||
getParentRoute: () => AuthenticatedSidebarLayoutRoute,
|
||||
} as any)
|
||||
const AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute =
|
||||
AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRouteImport.update({
|
||||
id: '/trash/directories/$directoryId',
|
||||
path: '/trash/directories/$directoryId',
|
||||
getParentRoute: () => AuthenticatedSidebarLayoutRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/login': typeof LoginRoute
|
||||
'/sign-up': typeof SignUpRoute
|
||||
'/login/callback': typeof LoginCallbackRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/home': typeof AuthenticatedSidebarLayoutHomeRoute
|
||||
'/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
||||
'/trash/directories/$directoryId': typeof AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/login': typeof LoginRoute
|
||||
'/sign-up': typeof SignUpRoute
|
||||
'/login/callback': typeof LoginCallbackRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/home': typeof AuthenticatedSidebarLayoutHomeRoute
|
||||
'/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
||||
'/trash/directories/$directoryId': typeof AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/_authenticated': typeof AuthenticatedRouteWithChildren
|
||||
'/login': typeof LoginRoute
|
||||
'/sign-up': typeof SignUpRoute
|
||||
'/_authenticated/_sidebar-layout': typeof AuthenticatedSidebarLayoutRouteWithChildren
|
||||
'/login_/callback': typeof LoginCallbackRoute
|
||||
'/_authenticated/': typeof AuthenticatedIndexRoute
|
||||
'/_authenticated/_sidebar-layout/home': typeof AuthenticatedSidebarLayoutHomeRoute
|
||||
'/_authenticated/_sidebar-layout/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
||||
'/_authenticated/_sidebar-layout/trash/directories/$directoryId': typeof AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/login'
|
||||
| '/sign-up'
|
||||
| '/login/callback'
|
||||
| '/'
|
||||
| '/home'
|
||||
| '/directories/$directoryId'
|
||||
| '/trash/directories/$directoryId'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/login' | '/login/callback' | '/' | '/home' | '/directories/$directoryId'
|
||||
to:
|
||||
| '/login'
|
||||
| '/sign-up'
|
||||
| '/login/callback'
|
||||
| '/'
|
||||
| '/home'
|
||||
| '/directories/$directoryId'
|
||||
| '/trash/directories/$directoryId'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/_authenticated'
|
||||
| '/login'
|
||||
| '/sign-up'
|
||||
| '/_authenticated/_sidebar-layout'
|
||||
| '/login_/callback'
|
||||
| '/_authenticated/'
|
||||
| '/_authenticated/_sidebar-layout/home'
|
||||
| '/_authenticated/_sidebar-layout/directories/$directoryId'
|
||||
| '/_authenticated/_sidebar-layout/trash/directories/$directoryId'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
|
||||
LoginRoute: typeof LoginRoute
|
||||
SignUpRoute: typeof SignUpRoute
|
||||
LoginCallbackRoute: typeof LoginCallbackRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/sign-up': {
|
||||
id: '/sign-up'
|
||||
path: '/sign-up'
|
||||
fullPath: '/sign-up'
|
||||
preLoaderRoute: typeof SignUpRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/login': {
|
||||
id: '/login'
|
||||
path: '/login'
|
||||
@@ -156,12 +194,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport
|
||||
parentRoute: typeof AuthenticatedSidebarLayoutRoute
|
||||
}
|
||||
'/_authenticated/_sidebar-layout/trash/directories/$directoryId': {
|
||||
id: '/_authenticated/_sidebar-layout/trash/directories/$directoryId'
|
||||
path: '/trash/directories/$directoryId'
|
||||
fullPath: '/trash/directories/$directoryId'
|
||||
preLoaderRoute: typeof AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRouteImport
|
||||
parentRoute: typeof AuthenticatedSidebarLayoutRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthenticatedSidebarLayoutRouteChildren {
|
||||
AuthenticatedSidebarLayoutHomeRoute: typeof AuthenticatedSidebarLayoutHomeRoute
|
||||
AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute: typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute
|
||||
AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute: typeof AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute
|
||||
}
|
||||
|
||||
const AuthenticatedSidebarLayoutRouteChildren: AuthenticatedSidebarLayoutRouteChildren =
|
||||
@@ -169,6 +215,8 @@ const AuthenticatedSidebarLayoutRouteChildren: AuthenticatedSidebarLayoutRouteCh
|
||||
AuthenticatedSidebarLayoutHomeRoute: AuthenticatedSidebarLayoutHomeRoute,
|
||||
AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute:
|
||||
AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute,
|
||||
AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute:
|
||||
AuthenticatedSidebarLayoutTrashDirectoriesDirectoryIdRoute,
|
||||
}
|
||||
|
||||
const AuthenticatedSidebarLayoutRouteWithChildren =
|
||||
@@ -193,6 +241,7 @@ const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
AuthenticatedRoute: AuthenticatedRouteWithChildren,
|
||||
LoginRoute: LoginRoute,
|
||||
SignUpRoute: SignUpRoute,
|
||||
LoginCallbackRoute: LoginCallbackRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import "@/styles/globals.css"
|
||||
import { ConvexProviderWithAuthKit } from "@convex-dev/workos"
|
||||
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { createRootRoute, Outlet } from "@tanstack/react-router"
|
||||
import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react"
|
||||
import { ConvexReactClient } from "convex/react"
|
||||
import { toast } from "sonner"
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { formatError } from "@/lib/error"
|
||||
import { useKeyboardModifierListener } from "@/lib/keyboard"
|
||||
import { authClient } from "../auth"
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootLayout,
|
||||
})
|
||||
|
||||
const convexClient = new ConvexReactClient(process.env.BUN_PUBLIC_CONVEX_URL!)
|
||||
const convexClient = new ConvexReactClient(process.env.BUN_PUBLIC_CONVEX_URL!, {
|
||||
verbose: true,
|
||||
})
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
mutations: {
|
||||
@@ -30,17 +33,13 @@ function RootLayout() {
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthKitProvider
|
||||
clientId={process.env.BUN_PUBLIC_WORKOS_CLIENT_ID!}
|
||||
redirectUri={process.env.BUN_PUBLIC_WORKOS_REDIRECT_URI!}
|
||||
<ConvexBetterAuthProvider
|
||||
client={convexClient}
|
||||
authClient={authClient}
|
||||
>
|
||||
<ConvexProviderWithAuthKit
|
||||
client={convexClient}
|
||||
useAuth={useAuth}
|
||||
>
|
||||
<Outlet />
|
||||
</ConvexProviderWithAuthKit>
|
||||
</AuthKitProvider>
|
||||
<Outlet />
|
||||
<Toaster />
|
||||
</ConvexBetterAuthProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Outlet,
|
||||
useLocation,
|
||||
} from "@tanstack/react-router"
|
||||
import { useAuth } from "@workos-inc/authkit-react"
|
||||
import {
|
||||
Authenticated,
|
||||
AuthLoading,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
useConvexAuth,
|
||||
} from "convex/react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { authClient, SessionContext } from "@/auth"
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner"
|
||||
|
||||
export const Route = createFileRoute("/_authenticated")({
|
||||
@@ -20,8 +20,8 @@ export const Route = createFileRoute("/_authenticated")({
|
||||
|
||||
function AuthenticatedLayout() {
|
||||
const { search } = useLocation()
|
||||
const { isLoading } = useConvexAuth()
|
||||
const { isLoading: authKitLoading } = useAuth()
|
||||
const { isLoading, isAuthenticated } = useConvexAuth()
|
||||
const { data: session, isPending: sessionLoading } = authClient.useSession()
|
||||
const [hasProcessedAuth, setHasProcessedAuth] = useState(false)
|
||||
|
||||
// Check if we're in the middle of processing an auth code
|
||||
@@ -29,17 +29,17 @@ function AuthenticatedLayout() {
|
||||
|
||||
// Track when auth processing is complete
|
||||
useEffect(() => {
|
||||
if (!authKitLoading && !isLoading) {
|
||||
if (!sessionLoading && !isLoading) {
|
||||
// Delay to ensure auth state is fully synchronized
|
||||
const timer = setTimeout(() => {
|
||||
setHasProcessedAuth(true)
|
||||
}, 500)
|
||||
}, 0)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [authKitLoading, isLoading])
|
||||
}, [sessionLoading, isLoading])
|
||||
|
||||
// Show loading during auth code processing or while auth state is syncing
|
||||
if (hasAuthCode || authKitLoading || isLoading || !hasProcessedAuth) {
|
||||
if (hasAuthCode || sessionLoading || isLoading || !hasProcessedAuth) {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<LoadingSpinner className="size-10" />
|
||||
@@ -50,10 +50,16 @@ function AuthenticatedLayout() {
|
||||
return (
|
||||
<>
|
||||
<Authenticated>
|
||||
<Outlet />
|
||||
{session ? (
|
||||
<SessionContext value={session}>
|
||||
<Outlet />
|
||||
</SessionContext>
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
</Authenticated>
|
||||
<Unauthenticated>
|
||||
<Navigate replace to="/login" />
|
||||
{/* <Navigate replace to="/login" /> */}
|
||||
</Unauthenticated>
|
||||
<AuthLoading>
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createFileRoute, Outlet } from "@tanstack/react-router"
|
||||
import { useQuery as useConvexQuery } from "convex/react"
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { DashboardSidebar } from "@/dashboard/dashboard-sidebar"
|
||||
@@ -17,7 +16,6 @@ function RouteComponent() {
|
||||
<Outlet />
|
||||
</SidebarInset>
|
||||
</div>
|
||||
<Toaster />
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,52 @@
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
|
||||
import {
|
||||
type FileSystemItem,
|
||||
FileType,
|
||||
newFileSystemHandle,
|
||||
} from "@fileone/convex/model/filesystem"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useQuery as useConvexQuery } from "convex/react"
|
||||
import type { Row, Table } from "@tanstack/react-table"
|
||||
import {
|
||||
useMutation as useContextMutation,
|
||||
useQuery as useConvexQuery,
|
||||
} from "convex/react"
|
||||
import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
PlusIcon,
|
||||
TextCursorInputIcon,
|
||||
TrashIcon,
|
||||
} from "lucide-react"
|
||||
import { useCallback, useContext } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { DirectoryIcon } from "@/components/icons/directory-icon"
|
||||
import { TextFileIcon } from "@/components/icons/text-file-icon"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { WithAtom } from "@/components/with-atom"
|
||||
import { DirectoryPageContext } from "@/directories/directory-page/context"
|
||||
import { DirectoryPage } from "@/directories/directory-page/directory-page"
|
||||
import { DirectoryContentTable } from "@/directories/directory-page/directory-content-table"
|
||||
import { DirectoryPageSkeleton } from "@/directories/directory-page/directory-page-skeleton"
|
||||
import { FilePathBreadcrumb } from "@/directories/directory-page/file-path-breadcrumb"
|
||||
import { NewDirectoryDialog } from "@/directories/directory-page/new-directory-dialog"
|
||||
import { RenameFileDialog } from "@/directories/directory-page/rename-file-dialog"
|
||||
import { FilePreviewDialog } from "@/files/file-preview-dialog"
|
||||
import { inProgressFileUploadCountAtom } from "@/files/store"
|
||||
import { UploadFileDialog } from "@/files/upload-file-dialog"
|
||||
import type { FileDragInfo } from "@/files/use-file-drop"
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticated/_sidebar-layout/directories/$directoryId",
|
||||
@@ -11,15 +54,88 @@ export const Route = createFileRoute(
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
enum DialogKind {
|
||||
NewDirectory = "NewDirectory",
|
||||
UploadFile = "UploadFile",
|
||||
}
|
||||
|
||||
type NewDirectoryDialogData = {
|
||||
kind: DialogKind.NewDirectory
|
||||
}
|
||||
|
||||
type UploadFileDialogData = {
|
||||
kind: DialogKind.UploadFile
|
||||
directory: Doc<"directories">
|
||||
}
|
||||
|
||||
type ActiveDialogData = NewDirectoryDialogData | UploadFileDialogData
|
||||
|
||||
// MARK: atoms
|
||||
const contextMenuTargetItemsAtom = atom<FileSystemItem[]>([])
|
||||
const activeDialogDataAtom = atom<ActiveDialogData | null>(null)
|
||||
const fileDragInfoAtom = atom<FileDragInfo | null>(null)
|
||||
const optimisticDeletedItemsAtom = atom(
|
||||
new Set<Id<"files"> | Id<"directories">>(),
|
||||
)
|
||||
const openedFileAtom = atom<Doc<"files"> | null>(null)
|
||||
const itemBeingRenamedAtom = atom<{
|
||||
originalItem: FileSystemItem
|
||||
name: string
|
||||
} | null>(null)
|
||||
|
||||
// MARK: page entry
|
||||
function RouteComponent() {
|
||||
const { directoryId } = Route.useParams()
|
||||
const rootDirectory = useConvexQuery(api.files.fetchRootDirectory)
|
||||
const directory = useConvexQuery(api.files.fetchDirectory, {
|
||||
directoryId,
|
||||
})
|
||||
const directoryContent = useConvexQuery(api.files.fetchDirectoryContent, {
|
||||
directoryId,
|
||||
})
|
||||
const store = useStore()
|
||||
const directoryContent = useConvexQuery(
|
||||
api.filesystem.fetchDirectoryContent,
|
||||
{
|
||||
directoryId,
|
||||
trashed: false,
|
||||
},
|
||||
)
|
||||
const setOpenedFile = useSetAtom(openedFileAtom)
|
||||
const setContextMenuTargetItems = useSetAtom(contextMenuTargetItemsAtom)
|
||||
|
||||
const tableFilter = useCallback(
|
||||
(item: FileSystemItem) =>
|
||||
store.get(optimisticDeletedItemsAtom).has(item.doc._id),
|
||||
[store],
|
||||
)
|
||||
|
||||
const openFile = useCallback(
|
||||
(file: Doc<"files">) => {
|
||||
setOpenedFile(file)
|
||||
},
|
||||
[setOpenedFile],
|
||||
)
|
||||
|
||||
const directoryUrlFn = useCallback(
|
||||
(directory: Doc<"directories">) => `/directories/${directory._id}`,
|
||||
[],
|
||||
)
|
||||
|
||||
const directoryUrlById = useCallback(
|
||||
(directoryId: Id<"directories">) => `/directories/${directoryId}`,
|
||||
[],
|
||||
)
|
||||
|
||||
const handleContextMenuRequest = (
|
||||
row: Row<FileSystemItem>,
|
||||
table: Table<FileSystemItem>,
|
||||
) => {
|
||||
if (row.getIsSelected()) {
|
||||
setContextMenuTargetItems(
|
||||
table.getSelectedRowModel().rows.map((row) => row.original),
|
||||
)
|
||||
} else {
|
||||
setContextMenuTargetItems([row.original])
|
||||
}
|
||||
}
|
||||
|
||||
if (!directory || !directoryContent || !rootDirectory) {
|
||||
return <DirectoryPageSkeleton />
|
||||
@@ -29,7 +145,256 @@ function RouteComponent() {
|
||||
<DirectoryPageContext
|
||||
value={{ rootDirectory, directory, directoryContent }}
|
||||
>
|
||||
<DirectoryPage />
|
||||
<header className="flex py-2 shrink-0 items-center gap-2 border-b px-4 w-full">
|
||||
<FilePathBreadcrumb
|
||||
rootLabel="All Files"
|
||||
directoryUrlFn={directoryUrlById}
|
||||
/>
|
||||
<div className="ml-auto flex flex-row gap-2">
|
||||
<NewDirectoryItemDropdown />
|
||||
<UploadFileButton />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* DirectoryContentContextMenu must wrap div instead of DirectoryContentTable, otherwise radix will throw "event.preventDefault is not a function" error, idk why */}
|
||||
<DirectoryContentContextMenu>
|
||||
<div className="w-full">
|
||||
<DirectoryContentTable
|
||||
filterFn={tableFilter}
|
||||
directoryUrlFn={directoryUrlFn}
|
||||
fileDragInfoAtom={fileDragInfoAtom}
|
||||
onContextMenu={handleContextMenuRequest}
|
||||
onOpenFile={openFile}
|
||||
/>
|
||||
</div>
|
||||
</DirectoryContentContextMenu>
|
||||
|
||||
<WithAtom atom={activeDialogDataAtom}>
|
||||
{(data, setData) => (
|
||||
<>
|
||||
<NewDirectoryDialog
|
||||
open={data?.kind === DialogKind.NewDirectory}
|
||||
directoryId={directory._id}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setData(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{data?.kind === DialogKind.UploadFile && (
|
||||
<UploadFileDialog
|
||||
targetDirectory={data.directory}
|
||||
onClose={() => setData(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</WithAtom>
|
||||
|
||||
<WithAtom atom={itemBeingRenamedAtom}>
|
||||
{(itemBeingRenamed, setItemBeingRenamed) => {
|
||||
if (!itemBeingRenamed) return null
|
||||
return (
|
||||
<RenameFileDialog
|
||||
item={itemBeingRenamed.originalItem}
|
||||
onRenameSuccess={() => {
|
||||
toast.success("File renamed successfully")
|
||||
setItemBeingRenamed(null)
|
||||
}}
|
||||
onClose={() => setItemBeingRenamed(null)}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</WithAtom>
|
||||
|
||||
<WithAtom atom={openedFileAtom}>
|
||||
{(openedFile, setOpenedFile) => {
|
||||
if (!openedFile) return null
|
||||
return (
|
||||
<FilePreviewDialog
|
||||
file={openedFile}
|
||||
onClose={() => setOpenedFile(null)}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</WithAtom>
|
||||
</DirectoryPageContext>
|
||||
)
|
||||
}
|
||||
|
||||
// ==================================
|
||||
// MARK: DirectoryContentContextMenu
|
||||
|
||||
function DirectoryContentContextMenu({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const store = useStore()
|
||||
const [target, setTarget] = useAtom(contextMenuTargetItemsAtom)
|
||||
const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom)
|
||||
const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash)
|
||||
|
||||
const { mutate: moveToTrash } = useMutation({
|
||||
mutationFn: moveToTrashMutation,
|
||||
onMutate: ({ handles }) => {
|
||||
setOptimisticDeletedItems(
|
||||
(prev) =>
|
||||
new Set([...prev, ...handles.map((handle) => handle.id)]),
|
||||
)
|
||||
},
|
||||
onSuccess: ({ deleted, errors }, { handles }) => {
|
||||
setOptimisticDeletedItems((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
for (const handle of handles) {
|
||||
newSet.delete(handle.id)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
if (errors.length === 0 && deleted.length === handles.length) {
|
||||
toast.success(`Moved ${handles.length} items to trash`)
|
||||
} else if (errors.length === handles.length) {
|
||||
toast.error("Failed to move to trash")
|
||||
} else {
|
||||
toast.info(
|
||||
`Moved ${deleted.length} items to trash; failed to move ${errors.length} items`,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const handleDelete = () => {
|
||||
const selectedItems = store.get(contextMenuTargetItemsAtom)
|
||||
if (selectedItems.length > 0) {
|
||||
moveToTrash({
|
||||
handles: selectedItems.map(newFileSystemHandle),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setTarget([])
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||
{target.length > 0 && (
|
||||
<ContextMenuContent>
|
||||
<RenameMenuItem />
|
||||
<ContextMenuItem
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<TrashIcon />
|
||||
Move to trash
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function RenameMenuItem() {
|
||||
const store = useStore()
|
||||
const target = useAtomValue(contextMenuTargetItemsAtom)
|
||||
const setItemBeingRenamed = useSetAtom(itemBeingRenamedAtom)
|
||||
|
||||
const handleRename = () => {
|
||||
const selectedItems = store.get(contextMenuTargetItemsAtom)
|
||||
if (selectedItems.length === 1) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: length is checked
|
||||
const selectedItem = selectedItems[0]!
|
||||
setItemBeingRenamed({
|
||||
originalItem: selectedItem,
|
||||
name: selectedItem.doc.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Only render if exactly one item is selected
|
||||
if (target.length !== 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuItem onClick={handleRename}>
|
||||
<TextCursorInputIcon />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
// ==================================
|
||||
|
||||
// tags: upload, uploadfile, uploadfilebutton, fileupload, fileuploadbutton
|
||||
function UploadFileButton() {
|
||||
const { directory } = useContext(DirectoryPageContext)
|
||||
const setActiveDialogData = useSetAtom(activeDialogDataAtom)
|
||||
const inProgressFileUploadCount = useAtomValue(
|
||||
inProgressFileUploadCountAtom,
|
||||
)
|
||||
|
||||
const handleClick = () => {
|
||||
setActiveDialogData({
|
||||
kind: DialogKind.UploadFile,
|
||||
directory: directory,
|
||||
})
|
||||
}
|
||||
|
||||
if (inProgressFileUploadCount > 0) {
|
||||
return (
|
||||
<Button size="sm" type="button" loading onClick={handleClick}>
|
||||
Uploading {inProgressFileUploadCount} files
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button size="sm" type="button" onClick={handleClick}>
|
||||
Upload files
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function NewDirectoryItemDropdown() {
|
||||
const [activeDialogData, setActiveDialogData] =
|
||||
useAtom(activeDialogDataAtom)
|
||||
|
||||
const addNewDirectory = () => {
|
||||
setActiveDialogData({
|
||||
kind: DialogKind.NewDirectory,
|
||||
})
|
||||
}
|
||||
|
||||
const handleCloseAutoFocus = (event: Event) => {
|
||||
// If we just created a new item, prevent the dropdown from restoring focus to the trigger
|
||||
if (activeDialogData) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" type="button" variant="outline">
|
||||
<PlusIcon className="size-4" />
|
||||
New
|
||||
<ChevronDownIcon className="pl-1 size-4 shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent onCloseAutoFocus={handleCloseAutoFocus}>
|
||||
<DropdownMenuItem>
|
||||
<TextFileIcon />
|
||||
Text file
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={addNewDirectory}>
|
||||
<DirectoryIcon />
|
||||
Directory
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
|
||||
import {
|
||||
type FileSystemItem,
|
||||
FileType,
|
||||
newFileSystemHandle,
|
||||
} from "@fileone/convex/model/filesystem"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import type { Row, Table } from "@tanstack/react-table"
|
||||
import {
|
||||
useMutation as useConvexMutation,
|
||||
useQuery as useConvexQuery,
|
||||
} from "convex/react"
|
||||
import { atom, useAtom, useSetAtom, useStore } from "jotai"
|
||||
import { ShredderIcon, TrashIcon, UndoIcon } from "lucide-react"
|
||||
import { useCallback, useContext, useEffect } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { WithAtom } from "@/components/with-atom"
|
||||
import { DirectoryPageContext } from "@/directories/directory-page/context"
|
||||
import { DirectoryContentTable } from "@/directories/directory-page/directory-content-table"
|
||||
import { DirectoryPageSkeleton } from "@/directories/directory-page/directory-page-skeleton"
|
||||
import { FilePathBreadcrumb } from "@/directories/directory-page/file-path-breadcrumb"
|
||||
import { FilePreviewDialog } from "@/files/file-preview-dialog"
|
||||
import type { FileDragInfo } from "@/files/use-file-drop"
|
||||
import { backgroundTaskProgressAtom } from "../../../dashboard/state"
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticated/_sidebar-layout/trash/directories/$directoryId",
|
||||
)({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
enum ActiveDialogKind {
|
||||
DeleteConfirmation = "DeleteConfirmation",
|
||||
EmptyTrashConfirmation = "EmptyTrashConfirmation",
|
||||
}
|
||||
|
||||
const contextMenuTargetItemsAtom = atom<FileSystemItem[]>([])
|
||||
const fileDragInfoAtom = atom<FileDragInfo | null>(null)
|
||||
const activeDialogAtom = atom<ActiveDialogKind | null>(null)
|
||||
const openedFileAtom = atom<Doc<"files"> | null>(null)
|
||||
const optimisticRemovedItemsAtom = atom(
|
||||
new Set<Id<"files"> | Id<"directories">>(),
|
||||
)
|
||||
|
||||
function RouteComponent() {
|
||||
const { directoryId } = Route.useParams()
|
||||
const rootDirectory = useConvexQuery(api.files.fetchRootDirectory)
|
||||
const directory = useConvexQuery(api.files.fetchDirectory, {
|
||||
directoryId,
|
||||
})
|
||||
const directoryContent = useConvexQuery(
|
||||
api.filesystem.fetchDirectoryContent,
|
||||
{
|
||||
directoryId,
|
||||
trashed: true,
|
||||
},
|
||||
)
|
||||
const setContextMenuTargetItems = useSetAtom(contextMenuTargetItemsAtom)
|
||||
const setOpenedFile = useSetAtom(openedFileAtom)
|
||||
|
||||
const directoryUrlFn = useCallback(
|
||||
(directory: Doc<"directories">) =>
|
||||
`/trash/directories/${directory._id}`,
|
||||
[],
|
||||
)
|
||||
|
||||
const directoryUrlById = useCallback(
|
||||
(directoryId: Id<"directories">) => `/trash/directories/${directoryId}`,
|
||||
[],
|
||||
)
|
||||
|
||||
if (!directory || !directoryContent || !rootDirectory) {
|
||||
return <DirectoryPageSkeleton />
|
||||
}
|
||||
|
||||
const handleContextMenuRequest = (
|
||||
row: Row<FileSystemItem>,
|
||||
table: Table<FileSystemItem>,
|
||||
) => {
|
||||
if (row.getIsSelected()) {
|
||||
setContextMenuTargetItems(
|
||||
table.getSelectedRowModel().rows.map((row) => row.original),
|
||||
)
|
||||
} else {
|
||||
setContextMenuTargetItems([row.original])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DirectoryPageContext
|
||||
value={{ rootDirectory, directory, directoryContent }}
|
||||
>
|
||||
<header className="flex py-2 shrink-0 items-center gap-2 border-b px-4 w-full">
|
||||
<FilePathBreadcrumb
|
||||
rootLabel="Trash"
|
||||
directoryUrlFn={directoryUrlById}
|
||||
/>
|
||||
<div className="ml-auto flex flex-row gap-2">
|
||||
<EmptyTrashButton />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<TableContextMenu>
|
||||
<div className="w-full">
|
||||
<DirectoryContentTable
|
||||
filterFn={() => true}
|
||||
directoryUrlFn={directoryUrlFn}
|
||||
fileDragInfoAtom={fileDragInfoAtom}
|
||||
onContextMenu={handleContextMenuRequest}
|
||||
onOpenFile={setOpenedFile}
|
||||
/>
|
||||
</div>
|
||||
</TableContextMenu>
|
||||
|
||||
<DeleteConfirmationDialog />
|
||||
<EmptyTrashConfirmationDialog />
|
||||
|
||||
<WithAtom atom={openedFileAtom}>
|
||||
{(openedFile, setOpenedFile) => {
|
||||
if (!openedFile) return null
|
||||
return (
|
||||
<FilePreviewDialog
|
||||
file={openedFile}
|
||||
onClose={() => setOpenedFile(null)}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</WithAtom>
|
||||
</DirectoryPageContext>
|
||||
)
|
||||
}
|
||||
|
||||
function TableContextMenu({ children }: React.PropsWithChildren) {
|
||||
const setActiveDialog = useSetAtom(activeDialogAtom)
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<RestoreContextMenuItem />
|
||||
<ContextMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setActiveDialog(ActiveDialogKind.DeleteConfirmation)
|
||||
}}
|
||||
>
|
||||
<ShredderIcon />
|
||||
Delete permanently
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function RestoreContextMenuItem() {
|
||||
const store = useStore()
|
||||
const setOptimisticRemovedItems = useSetAtom(optimisticRemovedItemsAtom)
|
||||
const restoreItemsMutation = useConvexMutation(api.filesystem.restoreItems)
|
||||
const { mutate: restoreItems, isPending: isRestoring } = useMutation({
|
||||
mutationFn: restoreItemsMutation,
|
||||
onMutate: ({ handles }) => {
|
||||
setOptimisticRemovedItems(
|
||||
new Set(handles.map((handle) => handle.id)),
|
||||
)
|
||||
},
|
||||
onSuccess: ({ restored, errors }) => {
|
||||
if (errors.length === 0) {
|
||||
if (restored.files > 0 && restored.directories > 0) {
|
||||
toast.success(
|
||||
`Restored ${restored.files} files and ${restored.directories} directories`,
|
||||
)
|
||||
} else if (restored.files > 0) {
|
||||
toast.success(`Restored ${restored.files} files`)
|
||||
} else if (restored.directories > 0) {
|
||||
toast.success(
|
||||
`Restored ${restored.directories} directories`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
toast.warning(
|
||||
`Restored ${restored.files} files and ${restored.directories} directories; failed to restore ${errors.length} items`,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
const setBackgroundTaskProgress = useSetAtom(backgroundTaskProgressAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (isRestoring) {
|
||||
setBackgroundTaskProgress({
|
||||
label: "Restoring items…",
|
||||
})
|
||||
} else {
|
||||
setBackgroundTaskProgress(null)
|
||||
}
|
||||
}, [isRestoring, setBackgroundTaskProgress])
|
||||
|
||||
const onClick = () => {
|
||||
const targetItems = store.get(contextMenuTargetItemsAtom)
|
||||
restoreItems({
|
||||
handles: targetItems.map(newFileSystemHandle),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuItem onClick={onClick}>
|
||||
<UndoIcon />
|
||||
Restore
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyTrashButton() {
|
||||
const setActiveDialog = useSetAtom(activeDialogAtom)
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setActiveDialog(ActiveDialogKind.EmptyTrashConfirmation)
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
Empty trash
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function DeleteConfirmationDialog() {
|
||||
const { rootDirectory } = useContext(DirectoryPageContext)
|
||||
const [activeDialog, setActiveDialog] = useAtom(activeDialogAtom)
|
||||
const [targetItems, setTargetItems] = useAtom(contextMenuTargetItemsAtom)
|
||||
const setOptimisticRemovedItems = useSetAtom(optimisticRemovedItemsAtom)
|
||||
|
||||
const deletePermanentlyMutation = useConvexMutation(
|
||||
api.filesystem.permanentlyDeleteItems,
|
||||
)
|
||||
const { mutate: deletePermanently, isPending: isDeleting } = useMutation({
|
||||
mutationFn: deletePermanentlyMutation,
|
||||
onMutate: ({ handles }) => {
|
||||
setOptimisticRemovedItems(
|
||||
(prev) =>
|
||||
new Set([...prev, ...handles.map((handle) => handle.id)]),
|
||||
)
|
||||
},
|
||||
onSuccess: ({ deleted, errors }, { handles }) => {
|
||||
setOptimisticRemovedItems((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
for (const handle of handles) {
|
||||
newSet.delete(handle.id)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
if (errors.length === 0) {
|
||||
toast.success(
|
||||
`Deleted ${deleted.files} files and ${deleted.directories} directories`,
|
||||
)
|
||||
} else {
|
||||
toast.warning(
|
||||
`Deleted ${deleted.files} files and ${deleted.directories} directories; failed to delete ${errors.length} items`,
|
||||
)
|
||||
}
|
||||
setActiveDialog(null)
|
||||
setTargetItems([])
|
||||
},
|
||||
})
|
||||
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
setActiveDialog(ActiveDialogKind.DeleteConfirmation)
|
||||
} else {
|
||||
setActiveDialog(null)
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
deletePermanently({
|
||||
handles:
|
||||
targetItems.length > 0
|
||||
? targetItems.map(newFileSystemHandle)
|
||||
: [
|
||||
newFileSystemHandle({
|
||||
kind: FileType.Directory,
|
||||
doc: rootDirectory,
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={activeDialog === ActiveDialogKind.DeleteConfirmation}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Permanently delete {targetItems.length} items?
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<p>
|
||||
{targetItems.length} items will be permanently deleted. They
|
||||
will be IRRECOVERABLE.
|
||||
</p>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline" disabled={isDeleting}>
|
||||
Go back
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
loading={isDeleting}
|
||||
>
|
||||
Yes, delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyTrashConfirmationDialog() {
|
||||
const [activeDialog, setActiveDialog] = useAtom(activeDialogAtom)
|
||||
|
||||
const { mutate: emptyTrash, isPending: isEmptying } = useMutation({
|
||||
mutationFn: useConvexMutation(api.filesystem.emptyTrash),
|
||||
onSuccess: () => {
|
||||
toast.success("Trash emptied successfully")
|
||||
setActiveDialog(null)
|
||||
},
|
||||
})
|
||||
|
||||
function onOpenChange(open: boolean) {
|
||||
if (open) {
|
||||
setActiveDialog(ActiveDialogKind.EmptyTrashConfirmation)
|
||||
} else {
|
||||
setActiveDialog(null)
|
||||
}
|
||||
}
|
||||
|
||||
function confirmEmpty() {
|
||||
emptyTrash(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={activeDialog === ActiveDialogKind.EmptyTrashConfirmation}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Empty your trash?</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<p>
|
||||
All items in the trash will be permanently deleted. They
|
||||
will be IRRECOVERABLE.
|
||||
</p>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline" disabled={isEmptying}>
|
||||
No, go back
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmEmpty}
|
||||
disabled={isEmptying}
|
||||
loading={isEmptying}
|
||||
>
|
||||
Yes, empty trash
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +1,184 @@
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useAuth } from "@workos-inc/authkit-react"
|
||||
import { Button } from "../components/ui/button"
|
||||
import { GalleryVerticalEnd } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { type AuthErrorCode, authClient, BetterAuthError } from "../auth"
|
||||
|
||||
export const Route = createFileRoute("/login")({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const { signIn } = useAuth()
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<Button onClick={() => signIn()}>Login</Button>
|
||||
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="flex w-full max-w-lg flex-col gap-6">
|
||||
<a
|
||||
href="#"
|
||||
className="flex items-center gap-2 self-center font-medium text-xl"
|
||||
>
|
||||
<div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
|
||||
<GalleryVerticalEnd className="size-4" />
|
||||
</div>
|
||||
Drexa
|
||||
</a>
|
||||
<LoginFormCard />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LoginFormCard({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Welcome back</CardTitle>
|
||||
<CardDescription>
|
||||
Login with your Apple or Google account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LoginForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FieldDescription className="px-6 text-center">
|
||||
By clicking continue, you agree to our{" "}
|
||||
<a href="#">Terms of Service</a> and{" "}
|
||||
<a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LoginForm() {
|
||||
const {
|
||||
mutate: signIn,
|
||||
isPending,
|
||||
error: signInError,
|
||||
} = useMutation({
|
||||
mutationFn: async ({
|
||||
email,
|
||||
password,
|
||||
}: {
|
||||
email: string
|
||||
password: string
|
||||
}) => {
|
||||
const { data: signInData, error } = await authClient.signIn.email({
|
||||
email,
|
||||
password,
|
||||
callbackURL: "/home",
|
||||
rememberMe: true,
|
||||
})
|
||||
if (error) {
|
||||
throw new BetterAuthError(error.code as AuthErrorCode)
|
||||
}
|
||||
return signInData
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const formData = new FormData(event.currentTarget)
|
||||
signIn({
|
||||
email: formData.get("email") as string,
|
||||
password: formData.get("password") as string,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<Button
|
||||
disabled={isPending}
|
||||
variant="outline"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<title>Apple logo</title>
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Login with Apple
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isPending}
|
||||
variant="outline"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<title>Google logo</title>
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Login with Google
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
|
||||
Or continue with
|
||||
</FieldSeparator>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
disabled={isPending}
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input
|
||||
disabled={isPending}
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button disabled={isPending} type="submit">
|
||||
{isPending ? "Logging in…" : "Login"}
|
||||
</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ function RouteComponent() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingConvexAuth && isAuthenticated && !isSyncingUser) {
|
||||
console.log({ isLoadingConvexAuth, isAuthenticated, isSyncingUser })
|
||||
syncUser(undefined, {
|
||||
onSuccess: () => {
|
||||
navigate({
|
||||
|
||||
218
packages/web/src/routes/sign-up.tsx
Normal file
218
packages/web/src/routes/sign-up.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||
import { GalleryVerticalEnd } from "lucide-react"
|
||||
import type React from "react"
|
||||
import { toast } from "sonner"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { type AuthErrorCode, authClient, BetterAuthError } from "../auth"
|
||||
|
||||
export const Route = createFileRoute("/sign-up")({
|
||||
component: SignupPage,
|
||||
})
|
||||
|
||||
type SignUpParams = {
|
||||
displayName: string
|
||||
email: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
class PasswordMismatchError extends Error {
|
||||
constructor() {
|
||||
super("Passwords do not match")
|
||||
}
|
||||
}
|
||||
|
||||
function SignupPage() {
|
||||
return (
|
||||
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="flex w-full max-w-xl flex-col gap-6">
|
||||
<a
|
||||
href="#"
|
||||
className="flex items-center gap-2 self-center font-medium text-xl"
|
||||
>
|
||||
<div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
|
||||
<GalleryVerticalEnd className="size-4" />
|
||||
</div>
|
||||
Drexa
|
||||
</a>
|
||||
<SignUpFormContainer>
|
||||
<SignupForm />
|
||||
</SignUpFormContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SignUpFormContainer({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">
|
||||
Create your account
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to create your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SignupForm() {
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
mutate: signUp,
|
||||
isPending,
|
||||
error: signUpError,
|
||||
} = useMutation({
|
||||
mutationFn: async (data: SignUpParams) => {
|
||||
if (data.password !== data.confirmPassword) {
|
||||
throw new PasswordMismatchError()
|
||||
}
|
||||
const { data: signUpData, error } = await authClient.signUp.email({
|
||||
name: data.displayName,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
})
|
||||
if (error) {
|
||||
throw new BetterAuthError(error.code as AuthErrorCode)
|
||||
}
|
||||
return signUpData
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate({
|
||||
to: "/",
|
||||
replace: true,
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error)
|
||||
toast.error("Unable to create your account")
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const formData = new FormData(event.currentTarget)
|
||||
signUp({
|
||||
displayName: formData.get("displayName") as string,
|
||||
email: formData.get("email") as string,
|
||||
password: formData.get("password") as string,
|
||||
confirmPassword: formData.get("confirmPassword") as string,
|
||||
})
|
||||
}
|
||||
|
||||
let passwordFieldError = null
|
||||
let emailFieldError = null
|
||||
if (signUpError instanceof BetterAuthError) {
|
||||
switch (signUpError.errorCode) {
|
||||
case "PASSWORD_TOO_SHORT":
|
||||
passwordFieldError =
|
||||
"Password must be at least 8 characters long"
|
||||
break
|
||||
case "INVALID_EMAIL":
|
||||
emailFieldError = "Invalid email address"
|
||||
break
|
||||
default:
|
||||
passwordFieldError = null
|
||||
}
|
||||
} else if (signUpError instanceof PasswordMismatchError) {
|
||||
passwordFieldError = "Passwords do not match"
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">Your Name</FieldLabel>
|
||||
<Input
|
||||
disabled={isPending}
|
||||
name="displayName"
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
required
|
||||
/>
|
||||
<FieldDescription>
|
||||
This is how you will be referred to on Drexa. You can
|
||||
change this later.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
disabled={isPending}
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
{emailFieldError ? (
|
||||
<FieldError>{emailFieldError}</FieldError>
|
||||
) : null}
|
||||
</Field>
|
||||
<Field>
|
||||
<Field className="grid grid-rows-2 md:grid-rows-1 md:grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<Input
|
||||
disabled={isPending}
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="confirm-password">
|
||||
Confirm Password
|
||||
</FieldLabel>
|
||||
<Input
|
||||
disabled={isPending}
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
</Field>
|
||||
{passwordFieldError ? (
|
||||
<FieldError>{passwordFieldError}</FieldError>
|
||||
) : (
|
||||
<FieldDescription>
|
||||
Must be at least 8 characters long.
|
||||
</FieldDescription>
|
||||
)}
|
||||
</Field>
|
||||
<Field>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isPending}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Creating account…" : "Create Account"}
|
||||
</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Already have an account? <a href="/sign-in">Sign in</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user