├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── components.json ├── package-lock.json ├── package.json ├── public │ ├── 001-excel.png │ ├── 002-png.png │ ├── 003-audio.png │ ├── 004-zip-file.png │ ├── 005-txt-file.png │ ├── 006-pdf.png │ ├── 007-source-code.png │ ├── 008-video.png │ ├── 009-img.png │ ├── 010-gif.png │ ├── bg3.png │ ├── bgpng.png │ ├── bgpng2.png │ ├── demo.MP4 │ ├── dragdrop.png │ ├── facicon.ico │ ├── index.html │ ├── key.png │ ├── logo192.png │ ├── manifest.json │ ├── ppt.png │ ├── robots.txt │ ├── updated.png │ ├── upgrade.png │ └── uploadingpng.png ├── src │ ├── app.tsx │ ├── assets │ │ ├── google.png │ │ └── jwt │ │ │ ├── circuit-vkey.json │ │ │ └── circuit.json │ ├── components │ │ ├── how-it-works-content.tsx │ │ ├── landing-page │ │ │ ├── content.tsx │ │ │ ├── footer.tsx │ │ │ ├── google-auth.tsx │ │ │ ├── header.tsx │ │ │ └── video-dialog.tsx │ │ ├── mode-toggle.tsx │ │ ├── privacy-content.tsx │ │ ├── protected-route.tsx │ │ ├── storage │ │ │ ├── confirmation-dialog.tsx │ │ │ ├── download-key.tsx │ │ │ ├── file-list.tsx │ │ │ ├── google-auth.tsx │ │ │ ├── header.tsx │ │ │ └── sidebar.tsx │ │ ├── terms-content.tsx │ │ ├── theme-provider.tsx │ │ └── ui │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── data-table.tsx │ │ │ ├── delete-dialog.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dialogKey.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── progress.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── sonner.tsx │ │ │ ├── spinner.tsx │ │ │ ├── table.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── tooltip.tsx │ │ │ └── use-toast.ts │ ├── index.css │ ├── index.js │ ├── lib │ │ ├── mime-types.tsx │ │ └── utils.ts │ ├── logo.svg │ ├── pages │ │ ├── google-oauth-callback.tsx │ │ ├── how-it-works.tsx │ │ ├── key-management-page.tsx │ │ ├── landing-page.tsx │ │ ├── privacy.tsx │ │ ├── private-storage.tsx │ │ └── terms.tsx │ ├── reportWebVitals.js │ ├── setupTests.js │ ├── types │ │ └── oauth.ts │ └── utils │ │ ├── cryptoUtils.ts │ │ ├── decryptFile.ts │ │ ├── dexieDB.ts │ │ ├── encryptFile.ts │ │ ├── fileOperations.ts │ │ └── gapiInit.ts ├── tailwind.config.js └── tsconfig.json ├── circuits ├── Nargo.toml ├── build.sh ├── src │ └── main.nr └── target │ ├── vk │ └── zerodrive_zk.json ├── components.json ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Shahad Pichen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZeroDrive 2 | 3 | **ZeroDrive** is a tool that keeps your Google Drive files secure with end-to-end encryption. Here’s what you need to know: 4 | 5 | ## Key Features 6 | 7 | - **End-to-End Encryption:** Your files are encrypted on your device before uploading to Google Drive. Only you can decrypt them. 8 | - **Open Source:** The code is fully available for review, ensuring transparency and trust. 9 | - **Easy to Use:** Drag, drop, and encrypt—no technical skills needed. 10 | - **Customizable:** Review, modify, and host the tool on your own servers if desired. 11 | - **IndexedDB Storage:** Efficient local storage management. 12 | 13 | ## Getting Started 14 | 15 | 1. **Clone the Repository:** 16 | 17 | ```bash 18 | git clone https://github.com/shahadpichen/zerodrive.git 19 | ``` 20 | 21 | 2. **Install Dependencies:** 22 | ```bash 23 | cd zerodrive 24 | npm install 25 | ``` 26 | 3. **Set Environment Variables:** 27 | 28 | Before starting the application, make sure to add the following environment variables: 29 | 30 | ```bash 31 | REACT_APP_PUBLIC_CLIENT_ID 32 | REACT_APP_PUBLIC_SCOPE 33 | ``` 34 | You can add these variables in a .env.local file in the project root. 35 | 5. **Run the Application:** 36 | ```bash 37 | npm start 38 | ``` 39 | 4. Visit localhost:3000 in your browser to start using ZeroDrive. 40 | 41 | ## Contact 42 | 43 | For support or questions, open an issue on GitHub 44 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "private-drive", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@radix-ui/react-avatar": "^1.1.1", 7 | "@radix-ui/react-checkbox": "^1.1.2", 8 | "@radix-ui/react-dialog": "^1.1.1", 9 | "@radix-ui/react-dropdown-menu": "^2.1.2", 10 | "@radix-ui/react-icons": "^1.3.1", 11 | "@radix-ui/react-label": "^2.1.0", 12 | "@radix-ui/react-progress": "^1.1.0", 13 | "@radix-ui/react-scroll-area": "^1.1.0", 14 | "@radix-ui/react-separator": "^1.1.0", 15 | "@radix-ui/react-slot": "^1.1.0", 16 | "@radix-ui/react-toast": "^1.2.1", 17 | "@radix-ui/react-tooltip": "^1.1.4", 18 | "@tanstack/react-table": "^8.20.5", 19 | "@tanstack/table-core": "^8.20.5", 20 | "@testing-library/jest-dom": "^5.17.0", 21 | "@testing-library/react": "^13.4.0", 22 | "@testing-library/user-event": "^13.5.0", 23 | "@types/gapi": "^0.0.47", 24 | "@types/gapi.auth2": "^0.0.61", 25 | "bip39": "^3.1.0", 26 | "buffer": "^6.0.3", 27 | "class-variance-authority": "^0.7.0", 28 | "clsx": "^2.1.1", 29 | "dexie": "^4.0.8", 30 | "dexie-cloud-addon": "^4.0.8", 31 | "gapi-script": "^1.2.0", 32 | "lucide-react": "^0.436.0", 33 | "markdown-to-jsx": "^7.5.0", 34 | "next-themes": "^0.3.0", 35 | "react": "^18.3.1", 36 | "react-dom": "^18.3.1", 37 | "react-icons": "^5.5.0", 38 | "react-router-dom": "^6.26.1", 39 | "react-scripts": "5.0.1", 40 | "sonner": "^1.5.0", 41 | "tailwind-merge": "^2.5.4", 42 | "tailwindcss-animate": "^1.0.7", 43 | "web-vitals": "^2.1.4" 44 | }, 45 | "scripts": { 46 | "start": "react-scripts start", 47 | "build": "react-scripts build", 48 | "test": "react-scripts test", 49 | "eject": "react-scripts eject" 50 | }, 51 | "eslintConfig": { 52 | "extends": [ 53 | "react-app", 54 | "react-app/jest" 55 | ] 56 | }, 57 | "browserslist": { 58 | "production": [ 59 | ">0.2%", 60 | "not dead", 61 | "not op_mini all" 62 | ], 63 | "development": [ 64 | "last 1 chrome version", 65 | "last 1 firefox version", 66 | "last 1 safari version" 67 | ] 68 | }, 69 | "devDependencies": { 70 | "@types/bip39": "^3.0.4", 71 | "tailwindcss": "^3.4.10" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/public/001-excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/001-excel.png -------------------------------------------------------------------------------- /app/public/002-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/002-png.png -------------------------------------------------------------------------------- /app/public/003-audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/003-audio.png -------------------------------------------------------------------------------- /app/public/004-zip-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/004-zip-file.png -------------------------------------------------------------------------------- /app/public/005-txt-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/005-txt-file.png -------------------------------------------------------------------------------- /app/public/006-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/006-pdf.png -------------------------------------------------------------------------------- /app/public/007-source-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/007-source-code.png -------------------------------------------------------------------------------- /app/public/008-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/008-video.png -------------------------------------------------------------------------------- /app/public/009-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/009-img.png -------------------------------------------------------------------------------- /app/public/010-gif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/010-gif.png -------------------------------------------------------------------------------- /app/public/bg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/bg3.png -------------------------------------------------------------------------------- /app/public/bgpng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/bgpng.png -------------------------------------------------------------------------------- /app/public/bgpng2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/bgpng2.png -------------------------------------------------------------------------------- /app/public/demo.MP4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/demo.MP4 -------------------------------------------------------------------------------- /app/public/dragdrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/dragdrop.png -------------------------------------------------------------------------------- /app/public/facicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/facicon.ico -------------------------------------------------------------------------------- /app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 32 | 33 | 37 | 38 | 47 | ZeroDrive 48 | 49 | 50 | 51 |
52 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/public/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/key.png -------------------------------------------------------------------------------- /app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/logo192.png -------------------------------------------------------------------------------- /app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /app/public/ppt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/ppt.png -------------------------------------------------------------------------------- /app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/public/updated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/updated.png -------------------------------------------------------------------------------- /app/public/upgrade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/upgrade.png -------------------------------------------------------------------------------- /app/public/uploadingpng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/public/uploadingpng.png -------------------------------------------------------------------------------- /app/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Routes, 5 | Route, 6 | Navigate, 7 | } from "react-router-dom"; 8 | import { Buffer } from "buffer/"; 9 | import PrivateStorage from "./pages/private-storage"; 10 | import LandingPage from "./pages/landing-page"; 11 | import ProtectedRoute from "./components/protected-route"; 12 | import Privacy from "./pages/privacy"; 13 | import Terms from "./pages/terms"; 14 | import HowItWorks from "./pages/how-it-works"; 15 | import GoogleOAuthCallback from "./pages/google-oauth-callback"; 16 | import { KeyManagementPage } from "./pages/key-management-page"; 17 | 18 | // Polyfill global Buffer for libraries that expect it (e.g., bip39) 19 | window.Buffer = Buffer as any; 20 | 21 | function App() { 22 | const isAuthenticated = localStorage.getItem("isAuthenticated") === "true"; 23 | 24 | return ( 25 | 26 | 27 | 32 | ) : ( 33 | 34 | ) 35 | } 36 | /> 37 | 41 | 42 | 43 | } 44 | /> 45 | 49 | 50 | 51 | } 52 | /> 53 | } /> 54 | } /> 55 | } /> 56 | } 59 | /> 60 | 61 | 62 | ); 63 | } 64 | 65 | export default App; 66 | -------------------------------------------------------------------------------- /app/src/assets/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahadpichen/zerodrive/a335476c4e6cd4b4d99addc81cc822eb5a7a3a6d/app/src/assets/google.png -------------------------------------------------------------------------------- /app/src/assets/jwt/circuit-vkey.json: -------------------------------------------------------------------------------- 1 | [ 2 | 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 20, 0, 3 | 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 188, 68, 3, 80, 0, 0, 127, 4 | 247, 26, 67, 177, 68, 0, 0, 127, 248, 147, 17, 17, 144, 0, 0, 127, 238, 8, 45, 5 | 64, 128, 0, 0, 0, 1, 147, 17, 17, 192, 0, 0, 127, 238, 0, 0, 0, 3, 0, 0, 0, 0, 6 | 188, 68, 3, 128, 0, 0, 127, 247, 7, 139, 111, 60, 65, 140, 89, 84, 6, 131, 9, 7 | 218, 231, 51, 166, 179, 153, 64, 15, 252, 20, 67, 146, 165, 226, 60, 7, 11, 8 | 69, 135, 114, 16, 41, 97, 105, 37, 176, 106, 237, 191, 28, 42, 21, 208, 208, 9 | 196, 219, 144, 154, 154, 179, 235, 185, 37, 202, 67, 151, 100, 144, 126, 32, 10 | 222, 147, 192, 15, 0, 17, 209, 228, 113, 184, 175, 130, 112, 177, 246, 245, 11 | 41, 136, 51, 185, 185, 44, 80, 230, 179, 93, 235, 50, 154, 251, 206, 161, 80, 12 | 51, 37, 32, 4, 84, 178, 99, 79, 34, 134, 73, 56, 163, 188, 58, 206, 52, 66, 13 | 45, 86, 229, 9, 106, 253, 216, 209, 186, 114, 7, 164, 2, 191, 207, 212, 20, 14 | 54, 200, 121, 35, 250, 219, 161, 85, 224, 234, 176, 234, 147, 66, 255, 21, 15 | 160, 24, 216, 142, 130, 124, 30, 141, 33, 237, 113, 25, 92, 210, 21, 13, 103, 16 | 149, 204, 127, 20, 215, 106, 169, 7, 210, 39, 192, 129, 200, 19, 170, 76, 48, 17 | 11, 3, 215, 122, 241, 240, 77, 194, 119, 56, 131, 81, 175, 0, 32, 204, 15, 8, 18 | 250, 247, 242, 175, 82, 62, 105, 91, 6, 52, 47, 165, 159, 157, 178, 70, 227, 19 | 88, 171, 58, 70, 189, 38, 201, 75, 188, 201, 28, 1, 40, 185, 3, 191, 4, 56, 20 | 248, 152, 199, 33, 109, 186, 153, 118, 44, 241, 29, 2, 27, 153, 115, 28, 117, 21 | 110, 103, 245, 175, 125, 106, 164, 0, 208, 47, 177, 66, 193, 185, 54, 126, 22 | 116, 187, 167, 129, 230, 151, 88, 29, 200, 207, 197, 106, 91, 50, 251, 230, 23 | 61, 91, 201, 45, 108, 4, 144, 42, 182, 131, 158, 95, 196, 119, 227, 198, 222, 24 | 240, 215, 190, 160, 119, 6, 132, 192, 210, 89, 245, 135, 188, 81, 121, 186, 25 | 16, 126, 178, 100, 217, 156, 0, 211, 217, 118, 105, 51, 249, 198, 141, 31, 26 | 244, 11, 209, 1, 167, 102, 219, 3, 6, 20, 237, 43, 33, 73, 240, 7, 130, 191, 27 | 57, 150, 45, 70, 34, 228, 171, 13, 217, 112, 58, 36, 119, 141, 137, 1, 140, 28 | 216, 86, 231, 184, 235, 209, 212, 4, 59, 56, 162, 87, 152, 71, 143, 53, 242, 29 | 38, 144, 19, 170, 60, 63, 44, 53, 11, 175, 126, 196, 44, 197, 168, 221, 244, 30 | 6, 138, 86, 220, 156, 234, 159, 184, 163, 224, 171, 82, 36, 196, 195, 185, 31 | 100, 36, 206, 161, 221, 120, 138, 172, 32, 68, 215, 220, 158, 122, 106, 136, 32 | 151, 123, 183, 24, 171, 51, 130, 51, 27, 250, 216, 4, 118, 13, 35, 127, 47, 33 | 30, 89, 53, 76, 102, 39, 27, 29, 194, 192, 138, 205, 151, 208, 125, 162, 39, 34 | 33, 56, 236, 81, 193, 120, 138, 157, 84, 44, 73, 126, 115, 164, 240, 46, 116, 35 | 44, 162, 41, 248, 175, 246, 103, 98, 133, 151, 212, 119, 142, 203, 11, 172, 36 | 175, 113, 209, 121, 86, 231, 93, 21, 39, 236, 250, 125, 95, 61, 1, 145, 202, 37 | 140, 161, 186, 39, 165, 229, 49, 39, 251, 1, 179, 151, 125, 111, 191, 43, 119, 38 | 103, 179, 168, 182, 209, 149, 112, 151, 15, 164, 190, 223, 31, 101, 87, 99, 39 | 219, 41, 114, 213, 182, 51, 97, 246, 62, 59, 32, 55, 153, 102, 122, 75, 177, 40 | 131, 252, 92, 173, 49, 146, 99, 245, 199, 238, 126, 14, 186, 254, 141, 109, 41 | 191, 252, 135, 24, 74, 154, 59, 14, 101, 22, 120, 161, 229, 65, 171, 87, 101, 42 | 33, 91, 232, 35, 35, 206, 225, 177, 18, 114, 46, 60, 114, 141, 255, 250, 105, 43 | 163, 41, 20, 211, 89, 72, 158, 114, 252, 101, 195, 206, 232, 79, 213, 22, 177, 44 | 31, 199, 113, 141, 86, 47, 97, 95, 11, 187, 223, 62, 184, 50, 59, 114, 165, 45 | 163, 184, 126, 211, 67, 123, 37, 47, 100, 124, 210, 47, 32, 217, 171, 55, 115, 46 | 180, 196, 27, 144, 201, 24, 27, 241, 84, 218, 75, 39, 46, 91, 144, 48, 133, 47 | 35, 109, 28, 135, 99, 123, 31, 192, 202, 205, 199, 168, 226, 228, 131, 135, 48 | 178, 69, 143, 71, 4, 36, 131, 42, 113, 91, 53, 164, 237, 247, 80, 156, 161, 49 | 101, 91, 202, 39, 183, 233, 194, 162, 7, 130, 217, 55, 226, 127, 249, 44, 123, 50 | 46, 107, 114, 2, 16, 120, 88, 90, 13, 196, 209, 134, 0, 2, 22, 152, 118, 95, 51 | 54, 253, 122, 168, 245, 151, 101, 40, 151, 3, 85, 138, 51, 222, 46, 242, 201, 52 | 1, 135, 3, 154, 58, 101, 15, 45, 88, 201, 118, 153, 34, 23, 135, 225, 50, 84, 53 | 199, 112, 97, 215, 148, 139, 230, 192, 17, 178, 169, 37, 61, 33, 41, 199, 123, 54 | 212, 33, 181, 148, 125, 138, 187, 124, 147, 141, 226, 33, 28, 2, 0, 103, 83, 55 | 201, 226, 163, 97, 70, 141, 64, 33, 78, 35, 155, 50, 11, 66, 27, 162, 141, 56 | 111, 37, 173, 1, 91, 112, 181, 26, 15, 48, 83, 80, 115, 68, 251, 28, 212, 186, 57 | 165, 66, 239, 3, 76, 83, 211, 206, 13, 13, 13, 180, 59, 134, 160, 189, 235, 58 | 86, 249, 223, 94, 239, 75, 33, 187, 218, 49, 178, 245, 215, 101, 140, 128, 59 | 200, 44, 52, 36, 59, 51, 106, 140, 12, 77, 93, 80, 205, 98, 210, 19, 136, 68, 60 | 13, 2, 125, 73, 101, 82, 222, 126, 246, 153, 222, 23, 107, 155, 12, 151, 194, 61 | 62, 10, 83, 125, 18, 28, 150, 108, 6, 34, 131, 4, 251, 87, 228, 151, 147, 26, 62 | 92, 123, 3, 216, 139, 110, 142, 255, 233, 246, 201, 211, 181, 155, 254, 165, 63 | 107, 125, 200, 26, 56, 52, 37, 105, 203, 113, 101, 197, 95, 3, 54, 171, 62, 64 | 19, 58, 231, 106, 23, 118, 13, 55, 32, 124, 33, 12, 122, 238, 106, 206, 89, 65 | 201, 32, 147, 114, 120, 96, 93, 168, 74, 14, 81, 191, 36, 101, 88, 49, 57, 66 | 177, 28, 176, 186, 244, 165, 187, 52, 93, 185, 200, 87, 146, 227, 93, 176, 31, 67 | 145, 233, 78, 0, 200, 140, 84, 41, 215, 112, 203, 201, 179, 5, 83, 88, 190, 68 | 130, 79, 206, 168, 228, 207, 216, 220, 180, 104, 155, 165, 29, 123, 0, 145, 69 | 120, 101, 189, 55, 200, 241, 137, 39, 147, 216, 44, 41, 227, 204, 206, 194, 70 | 253, 58, 248, 207, 225, 115, 25, 9, 59, 75, 170, 168, 7, 6, 46, 245, 16, 166, 71 | 241, 204, 102, 216, 198, 197, 80, 246, 13, 247, 67, 212, 205, 101, 20, 180, 72 | 169, 243, 246, 219, 116, 161, 71, 153, 115, 195, 19, 222, 44, 211, 28, 231, 73 | 29, 201, 97, 29, 96, 212, 53, 177, 200, 96, 19, 169, 240, 47, 125, 85, 190, 74 | 81, 131, 48, 113, 97, 120, 62, 66, 132, 19, 76, 32, 101, 236, 232, 207, 32, 75 | 44, 192, 113, 142, 215, 135, 93, 42, 117, 90, 251, 118, 202, 213, 47, 243, 76 | 138, 165, 87, 221, 245, 145, 222, 0, 28, 218, 45, 18, 10, 62, 199, 165, 160, 77 | 203, 179, 40, 187, 102, 237, 166, 108, 200, 169, 103, 163, 88, 30, 235, 28, 78 | 192, 81, 65, 189, 196, 243, 225, 251, 247, 46, 130, 203, 151, 56, 105, 187, 79 | 131, 234, 129, 23, 95, 239, 37, 223, 38, 145, 71, 114, 0, 233, 33, 217, 225, 80 | 211, 234, 215, 153, 66, 152, 89, 77, 20, 221, 119, 245, 141, 137, 151, 19, 81 | 159, 237, 111, 113, 23, 224, 12, 201, 67, 160, 61, 105, 159, 169, 154, 107, 82 | 143, 48, 158, 230, 195, 3, 170, 72, 40, 31, 118, 206, 182, 147, 139, 58, 178, 83 | 196, 33, 113, 242, 32, 192, 136, 7, 123, 191, 207, 64, 213, 92, 154, 0, 99, 84 | 227, 58, 210, 180, 185, 44, 9, 101, 14, 52, 87, 102, 146, 50, 191, 252, 230, 85 | 216, 24, 63, 201, 173, 7, 253, 132, 239, 45, 235, 185, 105, 83, 205, 151, 35, 86 | 165, 188, 212, 112, 1, 23, 237, 51, 60, 143, 123, 101, 201, 213, 47, 92, 35, 87 | 113, 197, 199, 95, 230, 157, 75, 247, 34, 235, 81, 123, 231, 56, 46, 175, 252, 88 | 178, 5, 26, 71, 160, 16, 65, 123, 223, 140, 8, 37, 190, 213, 101, 189, 41, 89 | 227, 37, 172, 142, 99, 1, 66, 127, 252, 186, 249, 225, 121, 14, 138, 41, 183, 90 | 28, 156, 7, 220, 130, 19, 196, 45, 15, 0, 201, 103, 181, 16, 228, 61, 34, 201, 91 | 161, 94, 20, 19, 162, 123, 12, 241, 246, 227, 130, 100, 153, 43, 0, 52, 212, 92 | 177, 77, 28, 248, 66, 49, 206, 113, 182, 180, 229, 88, 170, 179, 110, 152, 93 | 252, 222, 22, 206, 69, 146, 122, 28, 226, 179, 12, 121, 147, 37, 84, 55, 133, 94 | 7, 12, 25, 239, 117, 42, 150, 153, 210, 214, 75, 102, 119, 252, 47, 66, 100, 95 | 142, 48, 241, 113, 213, 9, 159, 8, 25, 233, 224, 34, 102, 183, 156, 52, 131, 96 | 177, 106, 172, 231, 122, 33, 190, 122, 233, 219, 9, 36, 118, 172, 164, 51, 97 | 111, 123, 143, 168, 36, 64, 23, 175, 163, 250, 30, 189, 116, 59, 43, 185, 74, 98 | 213, 76, 87, 158, 10, 69, 225, 153, 217, 142, 102, 11, 26, 124, 211, 70, 139, 99 | 178, 3, 5, 169, 191, 21, 196, 79, 17, 66, 248, 167, 115, 211, 106, 183, 177, 100 | 32, 244, 84, 116, 105, 233, 60, 113, 167, 102, 8, 225, 163, 84, 4, 150, 233, 101 | 238, 75, 242, 221, 103, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 102 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 103 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 12, 230, 104 | 10, 8, 92, 87, 29, 79, 54, 80, 140, 247, 35, 225, 107, 127, 252, 217, 113, 67, 105 | 195, 140, 97, 156, 201, 103, 88, 155, 135, 135, 194, 21, 57, 15, 58, 28, 171, 106 | 177, 221, 19, 167, 162, 153, 20, 66, 34, 39, 211, 109, 98, 196, 251, 99, 253, 107 | 58, 30, 54, 26, 41, 238, 250, 82, 15 108 | ] 109 | -------------------------------------------------------------------------------- /app/src/components/how-it-works-content.tsx: -------------------------------------------------------------------------------- 1 | export const HowItWorksContent = [ 2 | { 3 | heading: "How It Works", 4 | content: 5 | "A simple, privacy-focused solution for secure file storage on Google Drive. Our open-source tool **encrypts your files locally** on your device and stores them in your Google Account.", 6 | }, 7 | { 8 | heading: "Secure Your Google Drive Files", 9 | content: 10 | "Google Drive is convenient, but it lacks the privacy many users need. Our tool provides **end-to-end encryption**, ensuring that your files are **encrypted on your device** before they even leave. This way, **only you can access them**, no matter where they are stored. Your data remains private and secure, giving you peace of mind.", 11 | }, 12 | { 13 | heading: "Local Encryption Process", 14 | content: 15 | "Files are encrypted directly in your browser using **AES-GCM** (Advanced Encryption Standard in Galois/Counter Mode), a **highly secure encryption algorithm** that provides both confidentiality and authenticity. This **military-grade encryption** happens before any data is uploaded to Google Drive, ensuring your files are completely secured before leaving your device.", 16 | }, 17 | { 18 | heading: "Data Storage & Synchronization", 19 | content: 20 | "**Encrypted files** are stored on your Google Drive while file metadata is stored locally using **Dexie.js (IndexDB)**. To enable cross-browser access, this metadata is also **encrypted using the same AES-GCM encryption** and stored on Google Drive. During each login, ZeroDrive automatically fetches and syncs this encrypted metadata.", 21 | }, 22 | { 23 | heading: "Open Source Control", 24 | content: 25 | "As an **open source solution**, our tool puts you in the driver's seat. You can **review the source code**, **customize the implementation**, **host on your own servers**, and ensure it meets your specific security requirements. With ZeroDrive, you're not just a user—you have **complete control** over your data security.", 26 | }, 27 | { 28 | heading: "Key Features", 29 | content: 30 | "• **AES-GCM encryption**: Military-grade encryption for maximum security
• **Privacy through E2E encryption**: Your files are encrypted before leaving your device
• **Reliability of Google**: Leverage Google Drive's robust infrastructure
• **Freedom of open-source**: Full transparency and customization options
• **Cross-browser synchronization**: Access your files from any browser
• **Local encryption**: All encryption happens directly in your browser", 31 | }, 32 | { 33 | heading: "Technical Implementation", 34 | content: 35 | "ZeroDrive implements **AES-GCM encryption** using the **Web Crypto API**, providing a secure and standardized approach to cryptography. The encryption process, including **key generation and management**, happens entirely in your browser using JavaScript. **Each file is encrypted with a unique key**, and all encryption keys are themselves encrypted before being stored. We utilize **IndexDB** for local storage and **Google Drive's API** for cloud storage, creating a seamless and secure file management experience.", 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /app/src/components/landing-page/content.tsx: -------------------------------------------------------------------------------- 1 | export const content = [ 2 | { 3 | heading: "It’s Time to Secure Your Google Drive Files", 4 | description: 5 | "Google Drive is convenient, but it lacks the privacy many users need. Our tool provides end-to-end encryption, ensuring that your files are encrypted on your device before they even leave. This way, only you can access them, no matter where they are stored. Your data remains private and secure, giving you peace of mind.", 6 | }, 7 | { 8 | heading: "Take Full Control with Open Source", 9 | description: 10 | "As an open source solution, our tool puts you in the driver's seat. You can review, customize, and even host the tool on your own servers to ensure it meets your specific needs. With us, you're not just a user—you have complete control over your data security.", 11 | }, 12 | { 13 | description: 14 | "Privacy through E2E encryption; Reliability of Google; Freedom of open-source.", 15 | }, 16 | // { 17 | // description: "We hope you enjoy ZeroDrive!
Regards,
Shahad", 18 | // }, 19 | ]; 20 | -------------------------------------------------------------------------------- /app/src/components/landing-page/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | function Footer() { 5 | return ( 6 | 22 | ); 23 | } 24 | 25 | export default Footer; 26 | -------------------------------------------------------------------------------- /app/src/components/landing-page/google-auth.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { gapi } from "gapi-script"; 3 | import { Button } from "../ui/button"; 4 | import { FaGoogle } from "react-icons/fa"; 5 | 6 | interface GoogleAuthProps { 7 | onAuthChange: (authenticated: boolean) => void; 8 | theme?: "dark" | "light"; 9 | } 10 | 11 | export const GoogleAuth: React.FC = ({ 12 | onAuthChange, 13 | theme = "dark", 14 | }) => { 15 | const [isInitialized, setIsInitialized] = useState(false); 16 | 17 | useEffect(() => { 18 | const initClient = async () => { 19 | try { 20 | await new Promise((resolve) => { 21 | gapi.load("client:auth2", resolve); 22 | }); 23 | 24 | await gapi.client.init({ 25 | clientId: process.env.REACT_APP_PUBLIC_CLIENT_ID, 26 | scope: process.env.REACT_APP_PUBLIC_SCOPE, 27 | }); 28 | 29 | const authInstance = gapi.auth2.getAuthInstance(); 30 | 31 | // Check if already signed in 32 | if (authInstance.isSignedIn.get()) { 33 | localStorage.setItem("isAuthenticated", "true"); 34 | onAuthChange(true); 35 | window.location.href = "/storage"; 36 | } 37 | 38 | setIsInitialized(true); 39 | } catch (error) { 40 | console.error("Error initializing Google Auth:", error); 41 | } 42 | }; 43 | 44 | initClient(); 45 | }, [onAuthChange]); 46 | 47 | const handleSignIn = async () => { 48 | try { 49 | if (!isInitialized) return; 50 | 51 | const authInstance = gapi.auth2.getAuthInstance(); 52 | const user = await authInstance.signIn(); 53 | 54 | if (user) { 55 | localStorage.setItem("isAuthenticated", "true"); 56 | onAuthChange(true); 57 | window.location.href = "/storage"; 58 | } 59 | } catch (error) { 60 | console.error("Error signing in:", error); 61 | localStorage.removeItem("isAuthenticated"); 62 | onAuthChange(false); 63 | } 64 | }; 65 | 66 | return ( 67 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /app/src/components/landing-page/header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FaGithub } from "react-icons/fa"; 3 | import { AiTwotoneQuestionCircle } from "react-icons/ai"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { ModeToggle } from "../../components/mode-toggle"; 6 | 7 | function Header() { 8 | const navigate = useNavigate(); 9 | 10 | return ( 11 |
12 | navigate("/")} 14 | className="mr-6 flex items-center space-x-1 cursor-pointer" 15 | > 16 | {/* ZeroDrive Logo */} 17 | ZeroDrive 18 | 19 |
20 |

navigate("/how-it-works")} 23 | > 24 | {/* */} 25 | 26 | How it works 27 | 28 |

29 | 34 |

35 | {/* */} 36 | Github 37 |

38 |
39 | 40 |
41 |
42 | ); 43 | } 44 | 45 | export default Header; 46 | -------------------------------------------------------------------------------- /app/src/components/landing-page/video-dialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogTrigger, 6 | DialogTitle, 7 | DialogDescription, 8 | } from "../ui/dialog"; 9 | // import { BsFillPlayCircleFill } from "react-icons/bs"; 10 | 11 | export function VideoDialog() { 12 | const [videoError, setVideoError] = useState(false); 13 | const [isLoading, setIsLoading] = useState(true); 14 | 15 | return ( 16 | 17 | 18 | 22 | 23 | 24 | Product Demo Video 25 | 26 | A demonstration video showing the product features 27 | 28 | {isLoading && ( 29 |
30 |
31 |
32 | )} 33 | 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /app/src/components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | import { Button } from "./ui/button"; 3 | import { useTheme } from "./theme-provider"; 4 | 5 | export function ModeToggle() { 6 | const { theme, setTheme } = useTheme(); 7 | 8 | const toggleTheme = () => { 9 | setTheme(theme === "light" ? "dark" : "light"); 10 | }; 11 | 12 | return ( 13 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/components/privacy-content.tsx: -------------------------------------------------------------------------------- 1 | export const privacyPolicy = [ 2 | { 3 | heading: "Privacy Policy", 4 | content: 5 | "At ZeroDrive, we are committed to ensuring your privacy. This policy explains how we protect your data and what we do with the limited information you provide.", 6 | }, 7 | { 8 | heading: "Data Collection", 9 | content: 10 | "ZeroDrive does not collect or store any personal data. All files uploaded via ZeroDrive are encrypted locally on your device before being stored in your Google Drive account. We do not: collect or store your Google account details, access, view, or track the files you upload, or use third-party analytics or advertising tools that track user activity.", 11 | }, 12 | { 13 | heading: "Local Data Storage", 14 | content: 15 | "Encryption keys are stored locally in your browser's storage. These keys are never shared or uploaded to any external servers. You are responsible for the management of these keys. If they are lost, your encrypted data will be inaccessible.", 16 | }, 17 | { 18 | heading: "Google Drive Integration", 19 | content: 20 | "ZeroDrive acts as an intermediary to store encrypted files in your Google Drive account. While your files are encrypted, Google Drive’s privacy policy governs the storage of these files. Please review Google’s policies for more details on how they handle your data.", 21 | }, 22 | { 23 | heading: "No Third-Party Access", 24 | content: 25 | "We do not share your data with third parties. Since all encryption happens locally on your device, even we cannot access or view your files.", 26 | }, 27 | { 28 | heading: "Security", 29 | content: 30 | "We use industry-standard encryption to secure your data. However, no method of encryption is completely foolproof, and we cannot guarantee absolute security. Always ensure you follow best practices for key management and account security.", 31 | }, 32 | { 33 | heading: "Your Rights", 34 | content: 35 | "Since ZeroDrive does not collect personal data, there are no specific user rights regarding data access, correction, or deletion. However, you retain full control over your files stored in Google Drive.", 36 | }, 37 | { 38 | heading: "Changes to this Policy", 39 | content: 40 | "We may update this Privacy Policy from time to time. If we make significant changes, we will notify users accordingly.", 41 | }, 42 | { 43 | heading: "Contact Us", 44 | content: 45 | "For any questions or concerns regarding your privacy, please contact us.

Last updated: Thu 12 Sep", 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /app/src/components/protected-route.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Navigate } from "react-router-dom"; 3 | 4 | interface ProtectedRouteProps { 5 | children: React.ReactNode; 6 | isAuthenticated: boolean; 7 | redirectPath: string; 8 | } 9 | 10 | const ProtectedRoute: React.FC = ({ 11 | children, 12 | isAuthenticated, 13 | redirectPath, 14 | }) => { 15 | if (!isAuthenticated) { 16 | return ; 17 | } 18 | 19 | return <>{children}; 20 | }; 21 | 22 | export default ProtectedRoute; 23 | -------------------------------------------------------------------------------- /app/src/components/storage/confirmation-dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogClose, 10 | } from "../ui/dialog"; 11 | import { Button } from "../ui/button"; 12 | 13 | interface ConfirmationDialogProps { 14 | open: boolean; 15 | onOpenChange: (open: boolean) => void; 16 | title: string; 17 | description: string; 18 | onConfirm: () => void; 19 | confirmText?: string; 20 | cancelText?: string; 21 | } 22 | 23 | export const ConfirmationDialog: React.FC = ({ 24 | open, 25 | onOpenChange, 26 | title, 27 | description, 28 | onConfirm, 29 | confirmText = "Confirm", 30 | cancelText = "Cancel", 31 | }) => { 32 | const handleConfirm = () => { 33 | onConfirm(); 34 | onOpenChange(false); // Close the dialog after confirmation 35 | }; 36 | 37 | return ( 38 | 39 | 40 | 41 | {title} 42 | {description} 43 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /app/src/components/storage/download-key.tsx: -------------------------------------------------------------------------------- 1 | import { X } from "lucide-react"; 2 | 3 | // interface KeyManagementProps { 4 | // onClose?: () => void; 5 | // } 6 | 7 | // export const KeyManagement: React.FC = ({ onClose }) => { 8 | // const [error, setError] = useState(""); 9 | 10 | // const handleGenerateAndDownloadKey = async () => { 11 | // try { 12 | // const key = await generateKey(); 13 | // await storeKey(key); 14 | 15 | // const keyJWK = JSON.stringify(await crypto.subtle.exportKey("jwk", key)); 16 | 17 | // const blob = new Blob([keyJWK], { type: "application/json" }); 18 | // const url = URL.createObjectURL(blob); 19 | 20 | // const a = document.createElement("a"); 21 | // a.href = url; 22 | // a.download = "encryption-key.json"; 23 | // a.click(); 24 | // URL.revokeObjectURL(url); 25 | 26 | // toast.success("Encryption key generated", { 27 | // description: "Your encryption key has been generated and stored.", 28 | // }); 29 | // if (onClose) { 30 | // onClose(); 31 | // } else { 32 | // setTimeout(() => { 33 | // window.location.reload(); 34 | // }, 2000); 35 | // } 36 | // } catch (error) { 37 | // setError("Failed to generate encryption key. Please try again."); 38 | // } 39 | // }; 40 | 41 | // const handleFileUpload = async ( 42 | // event: React.ChangeEvent 43 | // ) => { 44 | // const file = event.target.files?.[0]; 45 | // if (file) { 46 | // const reader = new FileReader(); 47 | // reader.onload = async (e) => { 48 | // try { 49 | // const keyJWK = JSON.parse(e.target?.result as string); 50 | 51 | // if ( 52 | // !keyJWK.kty || 53 | // keyJWK.kty !== "oct" || 54 | // !keyJWK.k || 55 | // !keyJWK.alg || 56 | // keyJWK.alg !== "A256GCM" 57 | // ) { 58 | // throw new Error("Invalid key format"); 59 | // } 60 | 61 | // const key = await crypto.subtle.importKey( 62 | // "jwk", 63 | // keyJWK, 64 | // { name: "AES-GCM" }, 65 | // true, 66 | // ["encrypt", "decrypt"] 67 | // ); 68 | // await storeKey(key); 69 | // toast.success("Encryption key added", { 70 | // description: "Your encryption key has been added to storage.", 71 | // }); 72 | // if (onClose) { 73 | // onClose(); 74 | // } else { 75 | // setTimeout(() => { 76 | // window.location.reload(); 77 | // }, 2000); 78 | // } 79 | // } catch (error) { 80 | // setError( 81 | // "Invalid key file. Please ensure the file contains a valid AES-GCM key in JSON format." 82 | // ); 83 | // } 84 | // }; 85 | // reader.readAsText(file); 86 | // } 87 | // }; 88 | 89 | // return ( 90 | // <> 91 | //
92 | //
93 | // 94 | // {onClose && ( 95 | // 102 | // )} 103 | // 104 | // Encryption Key Management 105 | // 106 | // For first-time users, generate and download your encryption key. 107 | // Store it securely—losing it means losing access to your files. 108 | // 109 | // 110 | // 111 | //
112 | // 115 | 116 | //

----- Already have a key? -----

117 | //
118 | // 127 | //

{error}

128 | //
129 | //
130 | //
131 | //
132 | //
133 | // 134 | // ); 135 | // }; 136 | -------------------------------------------------------------------------------- /app/src/components/storage/google-auth.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { gapi } from "gapi-script"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | interface GoogleAuthProps { 6 | onAuthChange: (authenticated: boolean) => void; 7 | } 8 | 9 | export const GoogleAuth: React.FC = ({ onAuthChange }) => { 10 | const navigate = useNavigate(); 11 | 12 | useEffect(() => { 13 | const initClient = () => { 14 | gapi.client 15 | .init({ 16 | clientId: process.env.REACT_APP_PUBLIC_CLIENT_ID, 17 | scope: process.env.REACT_APP_PUBLIC_SCOPE, 18 | }) 19 | .then(() => { 20 | const authInstance = gapi.auth2.getAuthInstance(); 21 | 22 | const handleAuthChange = (signedIn: boolean) => { 23 | onAuthChange(signedIn); 24 | if (signedIn) { 25 | localStorage.setItem("isAuthenticated", "true"); 26 | navigate("/storage"); 27 | } 28 | }; 29 | 30 | authInstance.isSignedIn.listen(handleAuthChange); 31 | }); 32 | }; 33 | 34 | gapi.load("client:auth2", initClient); 35 | 36 | const renderSignInButton = () => { 37 | const buttonElement = document.getElementById("google-signin-button"); 38 | if (buttonElement) { 39 | gapi.signin2.render(buttonElement, { 40 | scope: process.env.REACT_APP_PUBLIC_SCOPE, 41 | width: 240, 42 | height: 50, 43 | longtitle: true, 44 | theme: "dark", 45 | onsuccess: () => { 46 | onAuthChange(true); 47 | localStorage.setItem("isAuthenticated", "true"); 48 | navigate("/storage"); 49 | }, 50 | onfailure: () => { 51 | onAuthChange(false); 52 | }, 53 | }); 54 | } 55 | }; 56 | 57 | renderSignInButton(); 58 | 59 | return () => { 60 | const buttonElement = document.getElementById("google-signin-button"); 61 | if (buttonElement) { 62 | buttonElement.innerHTML = ""; 63 | } 64 | }; 65 | }, [onAuthChange, navigate]); 66 | 67 | return
; 68 | }; 69 | -------------------------------------------------------------------------------- /app/src/components/storage/header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { gapi } from "gapi-script"; 3 | import { clearStoredKey } from "../../utils/cryptoUtils"; 4 | import { Avatar, AvatarImage, AvatarFallback } from "../ui/avatar"; 5 | import { Progress } from "../ui/progress"; 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuTrigger, 11 | } from "../ui/dropdown-menu"; 12 | import { Zap } from "lucide-react"; 13 | import { Button } from "../ui/button"; 14 | 15 | interface HeaderProps { 16 | setIsAuthenticated: (value: boolean) => void; 17 | } 18 | 19 | function Header({ setIsAuthenticated }: HeaderProps) { 20 | const [user, setUser] = React.useState<{ 21 | name: string; 22 | imageUrl: string; 23 | } | null>(null); 24 | const [storageInfo, setStorageInfo] = React.useState<{ 25 | used: number; 26 | total: number; 27 | } | null>(null); 28 | 29 | const formatBytes = (bytes: number) => { 30 | if (bytes === 0) return "0 Bytes"; 31 | const k = 1024; 32 | const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; 33 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 34 | return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; 35 | }; 36 | 37 | const loadUserAndStorageInfo = async () => { 38 | try { 39 | // Wait for GAPI to initialize 40 | await new Promise((resolve) => { 41 | gapi.load("client:auth2", resolve); 42 | }); 43 | 44 | // Initialize GAPI 45 | await gapi.client.init({ 46 | clientId: process.env.REACT_APP_PUBLIC_CLIENT_ID, 47 | scope: process.env.REACT_APP_PUBLIC_SCOPE, 48 | }); 49 | 50 | const authInstance = gapi.auth2.getAuthInstance(); 51 | 52 | if (!authInstance || !authInstance.isSignedIn.get()) { 53 | console.log("User not signed in"); 54 | setIsAuthenticated(false); 55 | window.location.href = "/"; 56 | return; 57 | } 58 | 59 | const currentUser = authInstance.currentUser.get(); 60 | const profile = currentUser.getBasicProfile(); 61 | 62 | if (!profile) { 63 | console.error("No profile found"); 64 | return; 65 | } 66 | 67 | setUser({ 68 | name: profile.getName(), 69 | imageUrl: profile.getImageUrl(), 70 | }); 71 | 72 | // Get fresh token 73 | const token = currentUser.getAuthResponse().access_token; 74 | 75 | // Set token for request 76 | gapi.client.setToken({ 77 | access_token: token, 78 | }); 79 | 80 | // Fetch storage information 81 | const response = await gapi.client.request({ 82 | path: "https://www.googleapis.com/drive/v3/about", 83 | params: { 84 | fields: "storageQuota", 85 | }, 86 | headers: { 87 | Authorization: `Bearer ${token}`, 88 | }, 89 | }); 90 | 91 | console.log(response); 92 | 93 | if (response.result.storageQuota) { 94 | const { storageQuota } = response.result; 95 | setStorageInfo({ 96 | used: parseInt(storageQuota.usage || "0"), 97 | total: parseInt(storageQuota.limit || "0"), 98 | }); 99 | } 100 | } catch (error) { 101 | console.error("Error loading user and storage info:", error); 102 | if (error.status === 401) { 103 | setIsAuthenticated(false); 104 | window.location.href = "/"; 105 | } 106 | } 107 | }; 108 | 109 | React.useEffect(() => { 110 | const initAndLoad = async () => { 111 | try { 112 | await loadUserAndStorageInfo(); 113 | } catch (error) { 114 | console.error("Error in initial load:", error); 115 | } 116 | }; 117 | 118 | initAndLoad(); 119 | 120 | const intervalId = setInterval(loadUserAndStorageInfo, 60 * 1000); 121 | return () => clearInterval(intervalId); 122 | }, [setIsAuthenticated]); 123 | 124 | const handleLogout = async () => { 125 | try { 126 | const authInstance = gapi.auth2.getAuthInstance(); 127 | if (authInstance) { 128 | await authInstance.signOut(); 129 | setIsAuthenticated(false); 130 | clearStoredKey(); 131 | localStorage.removeItem("isAuthenticated"); 132 | window.location.href = "/"; 133 | } 134 | } catch (error) { 135 | console.error("Error during logout:", error); 136 | } 137 | }; 138 | 139 | const getProgressColor = (percentage: number): string => { 140 | if (percentage < 60) return "hsl(142.1 76.2% 36.3%)"; // green-500 141 | if (percentage < 75) return "hsl(47.9 95.8% 53.1%)"; // yellow-500 142 | return "hsl(0 84.2% 60.2%)"; // red-500 143 | }; 144 | 145 | const usagePercentage = storageInfo 146 | ? (storageInfo.used / storageInfo.total) * 100 147 | : 0; 148 | 149 | return ( 150 |
151 |
152 | 162 |
163 | {storageInfo && ( 164 |
165 |
166 | 167 | 168 | Storage 169 | 170 | 171 | {formatBytes(storageInfo.used)} /{" "} 172 | {formatBytes(storageInfo.total)} 173 | 174 |
175 |
176 | 184 |
185 |
186 | )} 187 | 195 |
196 |
197 |
198 | ); 199 | } 200 | 201 | export default Header; 202 | -------------------------------------------------------------------------------- /app/src/components/storage/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { cn } from "../../lib/utils"; 3 | import { Cloud, Menu } from "lucide-react"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | type Section = "files" | "favorites" | "trash"; 7 | 8 | interface SidebarProps { 9 | activeSection: Section; 10 | setActiveSection: (section: Section) => void; 11 | } 12 | 13 | const sidebarItems = [ 14 | { 15 | id: "files" as const, 16 | label: "My Files", 17 | }, 18 | { 19 | id: "favorites" as const, 20 | label: "Favorites", 21 | }, 22 | { 23 | id: "trash" as const, 24 | label: "Recycle Bin", 25 | }, 26 | ] as const; 27 | 28 | export function Sidebar({ activeSection, setActiveSection }: SidebarProps) { 29 | const navigate = useNavigate(); 30 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 31 | 32 | const toggleMobileMenu = () => { 33 | setIsMobileMenuOpen(!isMobileMenuOpen); 34 | }; 35 | 36 | const SidebarContent = () => ( 37 | <> 38 |
39 |
40 | 41 | Your Vault 42 |
43 | 62 |
63 |
64 |
65 |

navigate("/privacy")} 67 | className="hover:underline cursor-pointer" 68 | > 69 | Privacy Policy 70 |

71 |

navigate("/terms")} 73 | className="hover:underline cursor-pointer" 74 | > 75 | Terms of Condition 76 |

77 |
78 |

Copyright © All Rights Reserved

79 |
80 | 81 | ); 82 | 83 | return ( 84 | <> 85 | {/* Mobile Hamburger Button */} 86 | 93 | 94 | {/* Mobile Sidebar */} 95 |
setIsMobileMenuOpen(false)} 101 | > 102 |
e.stopPropagation()} 108 | > 109 | 110 |
111 |
112 | 113 | {/* Desktop Sidebar */} 114 |
115 | 116 |
117 | 118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /app/src/components/terms-content.tsx: -------------------------------------------------------------------------------- 1 | export const termsOfService = [ 2 | { 3 | heading: "Terms of Service", 4 | content: 5 | "Thank you for choosing ZeroDrive!

When we say “company”, “we”, “our”, “us”, or “service” in this document, we are referring to ZeroDrive.", 6 | }, 7 | { 8 | heading: "Free, Open-Source Software", 9 | content: 10 | "ZeroDrive is a free and open-source service that encrypts your files locally on your device before storing them securely in your Google Drive account. We do not charge for this service, nor do we offer any paid plans. This service will remain free forever. By using ZeroDrive, now or in the future, you agree to the latest version of our Terms of Service. While we strive to keep things as transparent as possible, these terms may be updated over time. If there are significant changes, we will notify users accordingly.", 11 | }, 12 | { 13 | heading: "Account Terms", 14 | content: 15 | "You are responsible for maintaining the security of your Google Account used with ZeroDrive. We cannot be held liable for any loss or damage resulting from your failure to comply with this responsibility. You must use your Google Account to access and utilize ZeroDrive services. We do not provide alternative authentication methods. ZeroDrive is for lawful purposes only. You must not use the service in violation of any applicable laws.", 16 | }, 17 | { 18 | heading: "Data Privacy and Encryption", 19 | content: 20 | "Your files are encrypted locally using end-to-end encryption (E2E) before they are uploaded to Google Drive. Only you have access to the decryption key, and no one, including us, can decrypt or access your files. Additionally: We do not store any data on our servers. All encrypted files are stored directly in your Google Drive account, and the encryption keys are stored in your browser's local storage. You have full control over this data. We do not collect, track, or analyze your data. Your privacy is paramount.", 21 | }, 22 | { 23 | heading: "Content Ownership", 24 | content: 25 | "You own the content you upload to ZeroDrive and Google Drive. We do not claim any ownership of your files. Your data remains yours, and you can choose to delete it at any time.", 26 | }, 27 | { 28 | heading: "Security and Responsibility", 29 | content: 30 | "ZeroDrive employs strong encryption methods to secure your files. However: You are responsible for safeguarding your encryption key. If you lose your key, your data will be unrecoverable. We are not liable for any loss of data or security breaches that may occur due to third-party services, such as Google Drive, or misuse of your encryption key.", 31 | }, 32 | { 33 | heading: "General Conditions", 34 | content: 35 | "ZeroDrive is provided on an 'as is' and 'as available' basis. While we make every effort to ensure the stability and security of the service, we cannot guarantee it will always meet your specific requirements or expectations. As with any software, occasional bugs may arise. We do our best to address issues promptly.", 36 | }, 37 | { 38 | heading: "Limitation of Liability", 39 | content: 40 | "ZeroDrive shall not be liable to you or any third party for any direct, indirect, incidental, special, or consequential damages, including but not limited to, loss of data or unauthorized access to your files. You agree to use ZeroDrive at your own risk.", 41 | }, 42 | { 43 | heading: "Contact Us", 44 | content: 45 | "If you have any questions about these Terms of Service, feel free to reach out to us.

Last updated: Thu 12 Sep", 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /app/src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react"; 2 | 3 | type Theme = "dark" | "light" | "system"; 4 | 5 | type ThemeProviderProps = { 6 | children: React.ReactNode; 7 | defaultTheme?: Theme; 8 | storageKey?: string; 9 | }; 10 | 11 | type ThemeProviderState = { 12 | theme: Theme; 13 | setTheme: (theme: Theme) => void; 14 | }; 15 | 16 | const initialState: ThemeProviderState = { 17 | theme: "system", 18 | setTheme: () => null, 19 | }; 20 | 21 | const ThemeProviderContext = createContext(initialState); 22 | 23 | export function ThemeProvider({ 24 | children, 25 | defaultTheme = "system", 26 | storageKey = "vite-ui-theme", 27 | ...props 28 | }: ThemeProviderProps) { 29 | const [theme, setTheme] = useState( 30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme 31 | ); 32 | 33 | useEffect(() => { 34 | const root = window.document.documentElement; 35 | 36 | root.classList.remove("light", "dark"); 37 | 38 | if (theme === "system") { 39 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 40 | .matches 41 | ? "dark" 42 | : "light"; 43 | 44 | root.classList.add(systemTheme); 45 | return; 46 | } 47 | 48 | root.classList.add(theme); 49 | }, [theme]); 50 | 51 | const value = { 52 | theme, 53 | setTheme: (theme: Theme) => { 54 | localStorage.setItem(storageKey, theme); 55 | setTheme(theme); 56 | }, 57 | }; 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | ); 64 | } 65 | 66 | export const useTheme = () => { 67 | const context = useContext(ThemeProviderContext); 68 | 69 | if (context === undefined) 70 | throw new Error("useTheme must be used within a ThemeProvider"); 71 | 72 | return context; 73 | }; 74 | -------------------------------------------------------------------------------- /app/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | import { cn } from "../../lib/utils"; 6 | 7 | const Avatar = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | )); 20 | Avatar.displayName = AvatarPrimitive.Root.displayName; 21 | 22 | const AvatarImage = React.forwardRef< 23 | React.ElementRef, 24 | React.ComponentPropsWithoutRef 25 | >(({ className, ...props }, ref) => ( 26 | 31 | )); 32 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 33 | 34 | const AvatarFallback = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, ...props }, ref) => ( 38 | 46 | )); 47 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 48 | 49 | export { Avatar, AvatarImage, AvatarFallback }; 50 | -------------------------------------------------------------------------------- /app/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "../../lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /app/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "../../lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary border border-foreground hover:bg-foreground hover:text-primary", 14 | destructive: 15 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 16 | outline: "hover:text-accent-foreground", 17 | secondary: 18 | "bg-foreground text-primary hover:bg-primary hover:text-foreground border border-foreground", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-foreground underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-9 px-4 py-2", 24 | sm: "h-8 px-3 text-xs", 25 | lg: "h-10 px-8", 26 | icon: "h-9 w-9", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | } 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /app/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "../../lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
14 | )); 15 | Card.displayName = "Card"; 16 | 17 | const CardHeader = React.forwardRef< 18 | HTMLDivElement, 19 | React.HTMLAttributes 20 | >(({ className, ...props }, ref) => ( 21 |
26 | )); 27 | CardHeader.displayName = "CardHeader"; 28 | 29 | const CardTitle = React.forwardRef< 30 | HTMLParagraphElement, 31 | React.HTMLAttributes 32 | >(({ className, ...props }, ref) => ( 33 |

38 | )); 39 | CardTitle.displayName = "CardTitle"; 40 | 41 | const CardDescription = React.forwardRef< 42 | HTMLParagraphElement, 43 | React.HTMLAttributes 44 | >(({ className, ...props }, ref) => ( 45 |

50 | )); 51 | CardDescription.displayName = "CardDescription"; 52 | 53 | const CardContent = React.forwardRef< 54 | HTMLDivElement, 55 | React.HTMLAttributes 56 | >(({ className, ...props }, ref) => ( 57 |

58 | )); 59 | CardContent.displayName = "CardContent"; 60 | 61 | const CardFooter = React.forwardRef< 62 | HTMLDivElement, 63 | React.HTMLAttributes 64 | >(({ className, ...props }, ref) => ( 65 |
70 | )); 71 | CardFooter.displayName = "CardFooter"; 72 | 73 | export { 74 | Card, 75 | CardHeader, 76 | CardFooter, 77 | CardTitle, 78 | CardDescription, 79 | CardContent, 80 | }; 81 | -------------------------------------------------------------------------------- /app/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 3 | import { cn } from "../../lib/utils"; 4 | import { CheckIcon } from "@radix-ui/react-icons"; 5 | 6 | const Checkbox = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 21 | 22 | 23 | 24 | )); 25 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 26 | 27 | export { Checkbox }; 28 | -------------------------------------------------------------------------------- /app/src/components/ui/data-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { 5 | ColumnDef, 6 | flexRender, 7 | useReactTable, 8 | getCoreRowModel, 9 | getPaginationRowModel, 10 | } from "@tanstack/react-table"; 11 | import { Button } from "../ui/button"; 12 | import { 13 | Table, 14 | TableBody, 15 | TableCell, 16 | TableHead, 17 | TableHeader, 18 | TableRow, 19 | } from "./table"; 20 | import { Trash2 } from "lucide-react"; 21 | import { 22 | Dialog, 23 | DialogContent, 24 | DialogDescription, 25 | DialogFooter, 26 | DialogHeader, 27 | DialogTitle, 28 | DialogTrigger, 29 | } from "../ui/dialog"; 30 | import { getAllFilesForUser, sendToGoogleDrive } from "../../utils/dexieDB"; 31 | import { deleteAndSyncFile } from "../../utils/fileOperations"; 32 | import { toast } from "sonner"; 33 | import { gapi } from "gapi-script"; 34 | 35 | interface DataTableProps { 36 | columns: ColumnDef[]; 37 | data: TData[]; 38 | meta?: { 39 | updateData?: (newData: TData[]) => void; 40 | refetch?: () => void; 41 | downloadAndDecryptFile?: (fileId: string, fileName: string) => void; 42 | downloadingFileId?: string | null; 43 | }; 44 | } 45 | 46 | export function DataTable({ 47 | columns, 48 | data, 49 | meta, 50 | }: DataTableProps) { 51 | const [rowSelection, setRowSelection] = React.useState({}); 52 | const [open, setOpen] = React.useState(false); 53 | const [isDeleting, setIsDeleting] = React.useState(false); 54 | 55 | const table = useReactTable({ 56 | data, 57 | columns, 58 | getCoreRowModel: getCoreRowModel(), 59 | getPaginationRowModel: getPaginationRowModel(), 60 | onRowSelectionChange: setRowSelection, 61 | state: { 62 | rowSelection, 63 | }, 64 | meta: { 65 | ...meta, 66 | }, 67 | }); 68 | 69 | const handleDelete = async () => { 70 | setIsDeleting(true); 71 | const selectedRows = table.getFilteredSelectedRowModel().rows; 72 | const filesToDelete = selectedRows.map((row) => ({ 73 | id: (row.original as any).id, 74 | name: (row.original as any).name, // Assuming 'name' is available on the row data 75 | })); 76 | let deleteToastId: string | number | undefined; 77 | let allSucceeded = true; 78 | 79 | try { 80 | deleteToastId = toast.loading( 81 | `Deleting ${filesToDelete.length} selected file(s)...` 82 | ); 83 | 84 | const authInstance = gapi.auth2.getAuthInstance(); 85 | if (!authInstance || !authInstance.isSignedIn.get()) { 86 | throw new Error("Cannot fetch user email - not signed in."); 87 | } 88 | const userEmail = authInstance.currentUser 89 | .get() 90 | .getBasicProfile() 91 | .getEmail(); 92 | 93 | for (const file of filesToDelete) { 94 | const success = await deleteAndSyncFile(file.id, file.name, userEmail); 95 | if (!success) { 96 | allSucceeded = false; 97 | // deleteAndSyncFile shows individual errors, but we track overall success 98 | } 99 | } 100 | 101 | // Sync metadata only if deletion was attempted (files existed) 102 | // and at least one deletion might have succeeded (allSucceeded isn't guaranteed if loop was empty) 103 | if (filesToDelete.length > 0) { 104 | const updatedFiles = await getAllFilesForUser(userEmail); 105 | await sendToGoogleDrive(updatedFiles); 106 | } 107 | 108 | if (allSucceeded && filesToDelete.length > 0) { 109 | toast.success( 110 | `Deleted ${filesToDelete.length} file(s) and synced metadata.`, 111 | { 112 | id: deleteToastId, 113 | } 114 | ); 115 | } else if (filesToDelete.length > 0) { 116 | // Handle partial success or complete failure 117 | toast.warning( 118 | `Finished deleting. Some files may not have been removed. Check console for errors. Metadata synced. `, 119 | { id: deleteToastId } 120 | ); 121 | } else { 122 | // No files were selected or deleted 123 | toast.info("No files selected for deletion.", { id: deleteToastId }); 124 | } 125 | 126 | // Reload or refetch regardless of partial success to update the UI 127 | // Consider using meta?.refetch?.() if available instead of reload 128 | window.location.reload(); 129 | setRowSelection({}); // Clear selection after operation 130 | } catch (error: any) { 131 | console.error("Error deleting selected files:", error); 132 | toast.error("Failed to delete selected files", { 133 | description: error.message || "Unknown error", 134 | id: deleteToastId, 135 | }); 136 | } finally { 137 | setIsDeleting(false); 138 | setOpen(false); 139 | } 140 | }; 141 | 142 | return ( 143 |
144 | {table.getFilteredSelectedRowModel().rows.length > 0 && ( 145 |
146 | 147 | 148 | 152 | 153 | 154 | 155 | Are you sure? 156 | 157 | This action will delete{" "} 158 | {table.getFilteredSelectedRowModel().rows.length} selected 159 | file(s). 160 | 161 | 162 | 163 | 170 | 203 | 204 | 205 | 206 |
207 | )} 208 |
209 | 210 | 211 | {table.getHeaderGroups().map((headerGroup) => ( 212 | 213 | {headerGroup.headers.map((header) => ( 214 | 215 | {header.isPlaceholder 216 | ? null 217 | : flexRender( 218 | header.column.columnDef.header, 219 | header.getContext() 220 | )} 221 | 222 | ))} 223 | 224 | ))} 225 | 226 | 227 | {table.getRowModel().rows?.length ? ( 228 | table.getRowModel().rows.map((row) => ( 229 | 234 | {row.getVisibleCells().map((cell) => ( 235 | 236 | {flexRender( 237 | cell.column.columnDef.cell, 238 | cell.getContext() 239 | )} 240 | 241 | ))} 242 | 243 | )) 244 | ) : ( 245 | 246 | 250 | No results. 251 | 252 | 253 | )} 254 | 255 |
256 |
257 |
258 |
259 | {table.getFilteredSelectedRowModel().rows.length} of{" "} 260 | {table.getFilteredRowModel().rows.length} row(s) selected. 261 |
262 |
263 | 271 | 279 |
280 |
281 |
282 | ); 283 | } 284 | -------------------------------------------------------------------------------- /app/src/components/ui/delete-dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | } from "./dialog"; 10 | import { Button } from "./button"; 11 | 12 | interface DeleteDialogProps { 13 | open: boolean; 14 | onOpenChange: (open: boolean) => void; 15 | onDelete: () => Promise; 16 | isDeleting: boolean; 17 | itemCount: number; 18 | } 19 | 20 | export function DeleteDialog({ 21 | open, 22 | onOpenChange, 23 | onDelete, 24 | isDeleting, 25 | itemCount, 26 | }: DeleteDialogProps) { 27 | return ( 28 | 29 | 30 | 31 | Are you sure? 32 | 33 | This action will delete {itemCount} selected file(s). 34 | 35 | 36 | 37 | 44 | 77 | 78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /app/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import { Cross2Icon } from "@radix-ui/react-icons"; 4 | 5 | import { cn } from "../../lib/utils"; 6 | 7 | const Dialog = DialogPrimitive.Root; 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger; 10 | 11 | const DialogPortal = DialogPrimitive.Portal; 12 | 13 | const DialogClose = DialogPrimitive.Close; 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )); 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )); 52 | DialogContent.displayName = DialogPrimitive.Content.displayName; 53 | 54 | const DialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) => ( 58 |
65 | ); 66 | DialogHeader.displayName = "DialogHeader"; 67 | 68 | const DialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes) => ( 72 |
79 | ); 80 | DialogFooter.displayName = "DialogFooter"; 81 | 82 | const DialogTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )); 95 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 96 | 97 | const DialogDescription = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )); 107 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 108 | 109 | export { 110 | Dialog, 111 | DialogPortal, 112 | DialogOverlay, 113 | DialogTrigger, 114 | DialogClose, 115 | DialogContent, 116 | DialogHeader, 117 | DialogFooter, 118 | DialogTitle, 119 | DialogDescription, 120 | }; 121 | -------------------------------------------------------------------------------- /app/src/components/ui/dialogKey.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { Cross2Icon } from "@radix-ui/react-icons"; 6 | import { cn } from "../../lib/utils"; 7 | 8 | import { gapi } from "gapi-script"; 9 | 10 | const Dialog: React.FC<{ children: React.ReactNode }> = ({ children }) => { 11 | const [open, setOpen] = React.useState(false); 12 | const [isAuthenticated, setIsAuthenticated] = React.useState(false); 13 | 14 | React.useEffect(() => { 15 | const initClient = () => { 16 | gapi.client 17 | .init({ 18 | clientId: process.env.REACT_APP_PUBLIC_CLIENT_ID, 19 | scope: process.env.REACT_APP_PUBLIC_SCOPE, 20 | }) 21 | .then(() => { 22 | const authInstance = gapi.auth2.getAuthInstance(); 23 | setIsAuthenticated(authInstance.isSignedIn.get()); 24 | authInstance.isSignedIn.listen(setIsAuthenticated); 25 | 26 | // After initialization, check if dialog should open 27 | const storedKey = localStorage.getItem("aes-gcm-key"); 28 | if (!storedKey && authInstance.isSignedIn.get()) { 29 | setOpen(true); 30 | } 31 | }); 32 | }; 33 | 34 | gapi.load("client:auth2", initClient); 35 | }, []); 36 | 37 | const handleOpenChange = (isOpen: boolean) => { 38 | const storedKey = localStorage.getItem("aes-gcm-key"); 39 | if (storedKey && isAuthenticated) { 40 | setOpen(isOpen); 41 | } else { 42 | setOpen(true); 43 | } 44 | }; 45 | 46 | return ( 47 | 48 | {children} 49 | 50 | ); 51 | }; 52 | 53 | const DialogTrigger = DialogPrimitive.Trigger; 54 | 55 | const DialogPortal = DialogPrimitive.Portal; 56 | 57 | const DialogOverlay = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 69 | )); 70 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 71 | 72 | const DialogContent = React.forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >(({ className, children, ...props }, ref) => ( 76 | 77 | 78 | e.preventDefault()} 87 | onEscapeKeyDown={(e) => e.preventDefault()} 88 | > 89 | {children} 90 | {/* Conditionally render the close button based on the presence of the encryption key */} 91 | {localStorage.getItem("aes-gcm-key") && ( 92 | 93 | 94 | Close 95 | 96 | )} 97 | 98 | 99 | )); 100 | DialogContent.displayName = DialogPrimitive.Content.displayName; 101 | 102 | const DialogHeader = ({ 103 | className, 104 | ...props 105 | }: React.HTMLAttributes) => ( 106 |
113 | ); 114 | DialogHeader.displayName = "DialogHeader"; 115 | 116 | const DialogFooter = ({ 117 | className, 118 | ...props 119 | }: React.HTMLAttributes) => ( 120 |
127 | ); 128 | DialogFooter.displayName = "DialogFooter"; 129 | 130 | const DialogTitle = React.forwardRef< 131 | React.ElementRef, 132 | React.ComponentPropsWithoutRef 133 | >(({ className, ...props }, ref) => ( 134 | 142 | )); 143 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 144 | 145 | const DialogDescription = React.forwardRef< 146 | React.ElementRef, 147 | React.ComponentPropsWithoutRef 148 | >(({ className, ...props }, ref) => ( 149 | 154 | )); 155 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 156 | 157 | export { 158 | Dialog, 159 | DialogPortal, 160 | DialogOverlay, 161 | DialogTrigger, 162 | DialogContent, 163 | DialogHeader, 164 | DialogFooter, 165 | DialogTitle, 166 | DialogDescription, 167 | }; 168 | -------------------------------------------------------------------------------- /app/src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { 6 | CheckIcon, 7 | ChevronRightIcon, 8 | DotFilledIcon, 9 | } from "@radix-ui/react-icons"; 10 | import { cn } from "../../lib/utils"; 11 | 12 | const DropdownMenu = DropdownMenuPrimitive.Root; 13 | 14 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 15 | 16 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 17 | 18 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 19 | 20 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 21 | 22 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 23 | 24 | const DropdownMenuSubTrigger = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef & { 27 | inset?: boolean; 28 | } 29 | >(({ className, inset, children, ...props }, ref) => ( 30 | 39 | {children} 40 | 41 | 42 | )); 43 | DropdownMenuSubTrigger.displayName = 44 | DropdownMenuPrimitive.SubTrigger.displayName; 45 | 46 | const DropdownMenuSubContent = React.forwardRef< 47 | React.ElementRef, 48 | React.ComponentPropsWithoutRef 49 | >(({ className, ...props }, ref) => ( 50 | 58 | )); 59 | DropdownMenuSubContent.displayName = 60 | DropdownMenuPrimitive.SubContent.displayName; 61 | 62 | const DropdownMenuContent = React.forwardRef< 63 | React.ElementRef, 64 | React.ComponentPropsWithoutRef 65 | >(({ className, sideOffset = 4, ...props }, ref) => ( 66 | 67 | 77 | 78 | )); 79 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 80 | 81 | const DropdownMenuItem = React.forwardRef< 82 | React.ElementRef, 83 | React.ComponentPropsWithoutRef & { 84 | inset?: boolean; 85 | } 86 | >(({ className, inset, ...props }, ref) => ( 87 | 96 | )); 97 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 98 | 99 | const DropdownMenuCheckboxItem = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, children, checked, ...props }, ref) => ( 103 | 112 | 113 | 114 | 115 | 116 | 117 | {children} 118 | 119 | )); 120 | DropdownMenuCheckboxItem.displayName = 121 | DropdownMenuPrimitive.CheckboxItem.displayName; 122 | 123 | const DropdownMenuRadioItem = React.forwardRef< 124 | React.ElementRef, 125 | React.ComponentPropsWithoutRef 126 | >(({ className, children, ...props }, ref) => ( 127 | 135 | 136 | 137 | 138 | 139 | 140 | {children} 141 | 142 | )); 143 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 144 | 145 | const DropdownMenuLabel = React.forwardRef< 146 | React.ElementRef, 147 | React.ComponentPropsWithoutRef & { 148 | inset?: boolean; 149 | } 150 | >(({ className, inset, ...props }, ref) => ( 151 | 160 | )); 161 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 162 | 163 | const DropdownMenuSeparator = React.forwardRef< 164 | React.ElementRef, 165 | React.ComponentPropsWithoutRef 166 | >(({ className, ...props }, ref) => ( 167 | 172 | )); 173 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 174 | 175 | const DropdownMenuShortcut = ({ 176 | className, 177 | ...props 178 | }: React.HTMLAttributes) => { 179 | return ( 180 | 184 | ); 185 | }; 186 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 187 | 188 | export { 189 | DropdownMenu, 190 | DropdownMenuTrigger, 191 | DropdownMenuContent, 192 | DropdownMenuItem, 193 | DropdownMenuCheckboxItem, 194 | DropdownMenuRadioItem, 195 | DropdownMenuLabel, 196 | DropdownMenuSeparator, 197 | DropdownMenuShortcut, 198 | DropdownMenuGroup, 199 | DropdownMenuPortal, 200 | DropdownMenuSub, 201 | DropdownMenuSubContent, 202 | DropdownMenuSubTrigger, 203 | DropdownMenuRadioGroup, 204 | }; 205 | -------------------------------------------------------------------------------- /app/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "../../lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | } 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /app/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "../../lib/utils"; 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /app/src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 3 | 4 | import { cn } from "../../lib/utils"; 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 18 | 25 | 26 | )); 27 | Progress.displayName = ProgressPrimitive.Root.displayName; 28 | 29 | export { Progress }; 30 | -------------------------------------------------------------------------------- /app/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 3 | 4 | import { cn } from "../../lib/utils"; 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | )); 22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 23 | 24 | const ScrollBar = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, orientation = "vertical", ...props }, ref) => ( 28 | 41 | 42 | 43 | )); 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 45 | 46 | export { ScrollArea, ScrollBar }; 47 | -------------------------------------------------------------------------------- /app/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 3 | 4 | import { cn } from "../../lib/utils"; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref 13 | ) => ( 14 | 25 | ) 26 | ); 27 | Separator.displayName = SeparatorPrimitive.Root.displayName; 28 | 29 | export { Separator }; 30 | -------------------------------------------------------------------------------- /app/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTheme } from "next-themes"; 3 | import { Toaster as Sonner } from "sonner"; 4 | 5 | type ToasterProps = React.ComponentProps; 6 | 7 | const Toaster = ({ ...props }: ToasterProps) => { 8 | const { theme = "system" } = useTheme(); 9 | 10 | return ( 11 | 27 | ); 28 | }; 29 | 30 | export { Toaster }; 31 | -------------------------------------------------------------------------------- /app/src/components/ui/spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Spinner() { 4 | return ( 5 |
6 | 22 | Loading... 23 |
24 | ); 25 | } 26 | 27 | export default Spinner; 28 | -------------------------------------------------------------------------------- /app/src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "../../lib/utils"; 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )); 17 | Table.displayName = "Table"; 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )); 25 | TableHeader.displayName = "TableHeader"; 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )); 37 | TableBody.displayName = "TableBody"; 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )); 52 | TableFooter.displayName = "TableFooter"; 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )); 67 | TableRow.displayName = "TableRow"; 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
[role=checkbox]]:translate-y-[2px]", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )); 82 | TableHead.displayName = "TableHead"; 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | [role=checkbox]]:translate-y-[2px]", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )); 97 | TableCell.displayName = "TableCell"; 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes 102 | >(({ className, ...props }, ref) => ( 103 |
108 | )); 109 | TableCaption.displayName = "TableCaption"; 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | }; 121 | -------------------------------------------------------------------------------- /app/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "../../lib/utils"; 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |