mirror of
https://github.com/get-drexa/drive.git
synced 2025-11-30 21:41:39 +00:00
feat: initial bulk file upload dialog
This commit is contained in:
40
bun.lock
40
bun.lock
@@ -38,6 +38,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
@@ -52,8 +53,11 @@
|
|||||||
"convex": "^1.27.0",
|
"convex": "^1.27.0",
|
||||||
"convex-helpers": "^0.1.104",
|
"convex-helpers": "^0.1.104",
|
||||||
"jotai": "^2.14.0",
|
"jotai": "^2.14.0",
|
||||||
|
"jotai-effect": "^2.1.3",
|
||||||
|
"jotai-scope": "^0.9.5",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"motion": "^12.23.16",
|
"motion": "^12.23.16",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
@@ -238,6 +242,28 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@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-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-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=="],
|
||||||
@@ -302,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-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-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=="],
|
"@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=="],
|
||||||
@@ -446,7 +474,9 @@
|
|||||||
|
|
||||||
"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=="],
|
"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-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
|
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
|
||||||
|
|
||||||
@@ -536,6 +566,10 @@
|
|||||||
|
|
||||||
"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": ["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=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
@@ -562,7 +596,7 @@
|
|||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"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=="],
|
"nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="],
|
||||||
|
|
||||||
@@ -694,6 +728,8 @@
|
|||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"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=="],
|
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
@@ -31,8 +32,11 @@
|
|||||||
"convex": "^1.27.0",
|
"convex": "^1.27.0",
|
||||||
"convex-helpers": "^0.1.104",
|
"convex-helpers": "^0.1.104",
|
||||||
"jotai": "^2.14.0",
|
"jotai": "^2.14.0",
|
||||||
|
"jotai-effect": "^2.1.3",
|
||||||
|
"jotai-scope": "^0.9.5",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"motion": "^12.23.16",
|
"motion": "^12.23.16",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function DialogOverlay({
|
|||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
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 }
|
||||||
@@ -7,7 +7,7 @@ export function WithAtom<Value>({
|
|||||||
atom: PrimitiveAtom<Value>
|
atom: PrimitiveAtom<Value>
|
||||||
children: (
|
children: (
|
||||||
value: Value,
|
value: Value,
|
||||||
setValue: (value: Value) => void,
|
setValue: (value: Value | ((current: Value) => Value)) => void,
|
||||||
) => React.ReactNode
|
) => React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const [value, setValue] = useAtom(atom)
|
const [value, setValue] = useAtom(atom)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarRail,
|
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar"
|
||||||
import { LoadingSpinner } from "../components/ui/loading-spinner"
|
import { LoadingSpinner } from "../components/ui/loading-spinner"
|
||||||
import { backgroundTaskProgressAtom } from "./state"
|
import { backgroundTaskProgressAtom } from "./state"
|
||||||
@@ -50,7 +49,6 @@ export function DashboardSidebar() {
|
|||||||
<BackgroundTaskProgressItem />
|
<BackgroundTaskProgressItem />
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
31
packages/web/src/files/store.ts
Normal file
31
packages/web/src/files/store.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { atom } from "jotai"
|
||||||
|
import { atomFamily } from "jotai/utils"
|
||||||
|
|
||||||
|
type FileUpload = {
|
||||||
|
id: string
|
||||||
|
progress: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fileUploadsAtom = atom<Record<string, FileUpload>>({})
|
||||||
|
|
||||||
|
export const fileUploadAtomFamily = atomFamily((id: string) =>
|
||||||
|
atom(
|
||||||
|
(get) => get(fileUploadsAtom)[id],
|
||||||
|
(get, set, progress: number) => {
|
||||||
|
const fileUploads = { ...get(fileUploadsAtom) }
|
||||||
|
fileUploads[id] = { id, progress }
|
||||||
|
set(fileUploadsAtom, fileUploads)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const clearFileUploadAtom = atom(null, (get, set, id: string) => {
|
||||||
|
const fileUploads = { ...get(fileUploadsAtom) }
|
||||||
|
delete fileUploads[id]
|
||||||
|
fileUploadAtomFamily.remove(id)
|
||||||
|
set(fileUploadsAtom, fileUploads)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const hasFileUploadsAtom = atom(
|
||||||
|
(get) => Object.keys(get(fileUploadsAtom)).length > 0,
|
||||||
|
)
|
||||||
319
packages/web/src/files/upload-file-dialog.tsx
Normal file
319
packages/web/src/files/upload-file-dialog.tsx
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
import type { Doc } from "@fileone/convex/_generated/dataModel"
|
||||||
|
import { useMutation } from "@tanstack/react-query"
|
||||||
|
import {
|
||||||
|
atom,
|
||||||
|
type PrimitiveAtom,
|
||||||
|
useAtom,
|
||||||
|
useAtomValue,
|
||||||
|
useSetAtom,
|
||||||
|
useStore,
|
||||||
|
} from "jotai"
|
||||||
|
import { atomEffect } from "jotai-effect"
|
||||||
|
import { 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 { WithAtom } from "@/components/with-atom"
|
||||||
|
import { clearFileUploadAtom, fileUploadAtomFamily } 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[]>([])
|
||||||
|
|
||||||
|
export function UploadFileDialog({
|
||||||
|
targetDirectory,
|
||||||
|
onClose,
|
||||||
|
}: UploadFileDialogProps) {
|
||||||
|
const formId = useId()
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const setPickedFiles = useSetAtom(pickedFilesAtom)
|
||||||
|
const clearFileUpload = useSetAtom(clearFileUploadAtom)
|
||||||
|
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 uploadFile = useUploadFile({
|
||||||
|
targetDirectory,
|
||||||
|
})
|
||||||
|
const { mutate: uploadFiles, isPending: isUploading } = useMutation({
|
||||||
|
mutationFn: async (files: PickedFile[]) => {
|
||||||
|
const promises = files.map((pickedFile) =>
|
||||||
|
uploadFile({
|
||||||
|
file: pickedFile.file,
|
||||||
|
onStart: () => {
|
||||||
|
store.set(fileUploadAtomFamily(pickedFile.id), 0)
|
||||||
|
},
|
||||||
|
onProgress: (progress) => {
|
||||||
|
store.set(fileUploadAtomFamily(pickedFile.id), progress)
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
clearFileUpload(pickedFile.id)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await Promise.all(promises)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("All files uploaded successfully")
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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 startFileUpload() {
|
||||||
|
const pickedFiles = store.get(pickedFilesAtom)
|
||||||
|
uploadFiles(pickedFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Upload file to "{targetDirectory.name}"
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Drag and drop files here or click to select files
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form id={formId} onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
multiple
|
||||||
|
type="file"
|
||||||
|
name="files"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadFileDropContainer>
|
||||||
|
<UploadFileArea onClick={openFilePicker} />
|
||||||
|
</UploadFileDropContainer>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<WithAtom atom={pickedFilesAtom}>
|
||||||
|
{(pickedFiles) => (
|
||||||
|
<>
|
||||||
|
{pickedFiles.length > 0 ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={openFilePicker}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
Select more files
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
onClick={startFileUpload}
|
||||||
|
disabled={isUploading}
|
||||||
|
loading={isUploading}
|
||||||
|
>
|
||||||
|
{pickedFiles.length === 0
|
||||||
|
? "Upload"
|
||||||
|
: `Upload ${pickedFiles.length} files`}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</WithAtom>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = fileUploadAtomFamily(pickedFile.id)
|
||||||
|
const fileUpload = useAtomValue(fileUploadAtom)
|
||||||
|
console.log("fileUpload", fileUpload)
|
||||||
|
const { file, id } = pickedFile
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
58
packages/web/src/files/use-upload-file.ts
Normal file
58
packages/web/src/files/use-upload-file.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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", () => {
|
||||||
|
console.log("load", xhr.response)
|
||||||
|
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
|
||||||
@@ -47,6 +47,7 @@ import { FilePathBreadcrumb } from "@/directories/directory-page/file-path-bread
|
|||||||
import { NewDirectoryDialog } from "@/directories/directory-page/new-directory-dialog"
|
import { NewDirectoryDialog } from "@/directories/directory-page/new-directory-dialog"
|
||||||
import { RenameFileDialog } from "@/directories/directory-page/rename-file-dialog"
|
import { RenameFileDialog } from "@/directories/directory-page/rename-file-dialog"
|
||||||
import { FilePreviewDialog } from "@/files/file-preview-dialog"
|
import { FilePreviewDialog } from "@/files/file-preview-dialog"
|
||||||
|
import { UploadFileDialog } from "@/files/upload-file-dialog"
|
||||||
import type { FileDragInfo } from "@/files/use-file-drop"
|
import type { FileDragInfo } from "@/files/use-file-drop"
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
@@ -55,9 +56,25 @@ export const Route = createFileRoute(
|
|||||||
component: RouteComponent,
|
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
|
// MARK: atoms
|
||||||
const contextMenuTargetItemsAtom = atom<FileSystemItem[]>([])
|
const contextMenuTargetItemsAtom = atom<FileSystemItem[]>([])
|
||||||
const newFileTypeAtom = atom<FileType | null>(null)
|
const activeDialogDataAtom = atom<ActiveDialogData | null>(null)
|
||||||
const fileDragInfoAtom = atom<FileDragInfo | null>(null)
|
const fileDragInfoAtom = atom<FileDragInfo | null>(null)
|
||||||
const optimisticDeletedItemsAtom = atom(
|
const optimisticDeletedItemsAtom = atom(
|
||||||
new Set<Id<"files"> | Id<"directories">>(),
|
new Set<Id<"files"> | Id<"directories">>(),
|
||||||
@@ -154,17 +171,25 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</DirectoryContentContextMenu>
|
</DirectoryContentContextMenu>
|
||||||
|
|
||||||
<WithAtom atom={newFileTypeAtom}>
|
<WithAtom atom={activeDialogDataAtom}>
|
||||||
{(newFileType, setNewFileType) => (
|
{(data, setData) => (
|
||||||
|
<>
|
||||||
<NewDirectoryDialog
|
<NewDirectoryDialog
|
||||||
open={newFileType === FileType.Directory}
|
open={data?.kind === DialogKind.NewDirectory}
|
||||||
directoryId={directory._id}
|
directoryId={directory._id}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setNewFileType(null)
|
setData(null)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{data?.kind === DialogKind.UploadFile && (
|
||||||
|
<UploadFileDialog
|
||||||
|
targetDirectory={data.directory}
|
||||||
|
onClose={() => setData(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</WithAtom>
|
</WithAtom>
|
||||||
|
|
||||||
@@ -249,8 +274,6 @@ function DirectoryContentContextMenu({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("target", target)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
@@ -311,82 +334,62 @@ function RenameMenuItem() {
|
|||||||
// tags: upload, uploadfile, uploadfilebutton, fileupload, fileuploadbutton
|
// tags: upload, uploadfile, uploadfilebutton, fileupload, fileuploadbutton
|
||||||
function UploadFileButton() {
|
function UploadFileButton() {
|
||||||
const { directory } = useContext(DirectoryPageContext)
|
const { directory } = useContext(DirectoryPageContext)
|
||||||
const generateUploadUrl = useConvexMutation(api.files.generateUploadUrl)
|
// const uploadFile = useUploadFile({
|
||||||
const saveFile = useConvexMutation(api.files.saveFile)
|
// targetDirectory: directory,
|
||||||
const { mutate: uploadFile, isPending: isUploading } = useMutation({
|
// })
|
||||||
mutationFn: async (file: File) => {
|
// const { mutate: uploadFile, isPending: isUploading } = useMutation({
|
||||||
const uploadUrl = await generateUploadUrl()
|
// mutationFn: async (file: File) => {
|
||||||
const uploadResult = await fetch(uploadUrl, {
|
// const uploadUrl = await generateUploadUrl()
|
||||||
method: "POST",
|
// const uploadResult = await fetch(uploadUrl, {
|
||||||
body: file,
|
// method: "POST",
|
||||||
headers: {
|
// body: file,
|
||||||
"Content-Type": file.type,
|
// headers: {
|
||||||
},
|
// "Content-Type": file.type,
|
||||||
})
|
// },
|
||||||
const { storageId } = await uploadResult.json()
|
// })
|
||||||
|
// const { storageId } = await uploadResult.json()
|
||||||
|
|
||||||
await saveFile({
|
// await saveFile({
|
||||||
storageId,
|
// storageId,
|
||||||
name: file.name,
|
// name: file.name,
|
||||||
size: file.size,
|
// size: file.size,
|
||||||
mimeType: file.type,
|
// mimeType: file.type,
|
||||||
directoryId: directory._id,
|
// directoryId: directory._id,
|
||||||
})
|
// })
|
||||||
},
|
// },
|
||||||
onSuccess: () => {
|
// onSuccess: () => {
|
||||||
toast.success("File uploaded successfully.")
|
// toast.success("File uploaded successfully.")
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
|
const setActiveDialogData = useSetAtom(activeDialogDataAtom)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
fileInputRef.current?.click()
|
setActiveDialogData({
|
||||||
}
|
kind: DialogKind.UploadFile,
|
||||||
|
directory: directory,
|
||||||
const onFileUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
})
|
||||||
const file = e.target.files?.[0]
|
|
||||||
if (file) {
|
|
||||||
uploadFile(file)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Button size="sm" type="button" onClick={handleClick}>
|
||||||
<input
|
Upload files
|
||||||
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>
|
</Button>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewDirectoryItemDropdown() {
|
function NewDirectoryItemDropdown() {
|
||||||
const [newFileType, setNewFileType] = useAtom(newFileTypeAtom)
|
const [activeDialogData, setActiveDialogData] =
|
||||||
|
useAtom(activeDialogDataAtom)
|
||||||
|
|
||||||
const addNewDirectory = () => {
|
const addNewDirectory = () => {
|
||||||
setNewFileType(FileType.Directory)
|
setActiveDialogData({
|
||||||
|
kind: DialogKind.NewDirectory,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCloseAutoFocus = (event: Event) => {
|
const handleCloseAutoFocus = (event: Event) => {
|
||||||
// If we just created a new item, prevent the dropdown from restoring focus to the trigger
|
// If we just created a new item, prevent the dropdown from restoring focus to the trigger
|
||||||
if (newFileType) {
|
if (activeDialogData) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user