├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── components.json ├── convex ├── README.md ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js ├── auth.config.ts ├── clerk.ts ├── crons.ts ├── files.ts ├── http.ts ├── schema.ts ├── tsconfig.json └── users.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── empty.svg ├── logo.png ├── next.svg └── vercel.svg ├── src ├── app │ ├── ConvexClientProvider.tsx │ ├── dashboard │ │ ├── _components │ │ │ ├── columns.tsx │ │ │ ├── file-actions.tsx │ │ │ ├── file-browser.tsx │ │ │ ├── file-card.tsx │ │ │ ├── file-table.tsx │ │ │ ├── search-bar.tsx │ │ │ └── upload-button.tsx │ │ ├── favorites │ │ │ └── page.tsx │ │ ├── files │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── side-nav.tsx │ │ └── trash │ │ │ └── page.tsx │ ├── favicon.ico │ ├── footer.tsx │ ├── globals.css │ ├── header.tsx │ ├── layout.tsx │ └── page.tsx ├── components │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── select.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── lib │ └── utils.ts └── middleware.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Web Dev Cody 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 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | 38 | ## TODO 39 | 40 | - shared with me 41 | - folders 42 | - landing page 43 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. See 4 | https://docs.convex.dev/using/writing-convex-functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | handler: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | handler: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result) 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.10.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | import type * as clerk from "../clerk.js"; 18 | import type * as crons from "../crons.js"; 19 | import type * as files from "../files.js"; 20 | import type * as http from "../http.js"; 21 | import type * as users from "../users.js"; 22 | 23 | /** 24 | * A utility for referencing Convex functions in your app's API. 25 | * 26 | * Usage: 27 | * ```js 28 | * const myFunctionReference = api.myModule.myFunction; 29 | * ``` 30 | */ 31 | declare const fullApi: ApiFromModules<{ 32 | clerk: typeof clerk; 33 | crons: typeof crons; 34 | files: typeof files; 35 | http: typeof http; 36 | users: typeof users; 37 | }>; 38 | export declare const api: FilterApi< 39 | typeof fullApi, 40 | FunctionReference 41 | >; 42 | export declare const internal: FilterApi< 43 | typeof fullApi, 44 | FunctionReference 45 | >; 46 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.10.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.10.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | DataModelFromSchemaDefinition, 14 | DocumentByName, 15 | TableNamesInDataModel, 16 | SystemTableNames, 17 | } from "convex/server"; 18 | import type { GenericId } from "convex/values"; 19 | import schema from "../schema.js"; 20 | 21 | /** 22 | * The names of all of your Convex tables. 23 | */ 24 | export type TableNames = TableNamesInDataModel; 25 | 26 | /** 27 | * The type of a document stored in Convex. 28 | * 29 | * @typeParam TableName - A string literal type of the table name (like "users"). 30 | */ 31 | export type Doc = DocumentByName< 32 | DataModel, 33 | TableName 34 | >; 35 | 36 | /** 37 | * An identifier for a document in Convex. 38 | * 39 | * Convex documents are uniquely identified by their `Id`, which is accessible 40 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 41 | * 42 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 43 | * 44 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 45 | * strings when type checking. 46 | * 47 | * @typeParam TableName - A string literal type of the table name (like "users"). 48 | */ 49 | export type Id = 50 | GenericId; 51 | 52 | /** 53 | * A type describing your Convex data model. 54 | * 55 | * This type includes information about what tables you have, the type of 56 | * documents stored in those tables, and the indexes defined on them. 57 | * 58 | * This type is used to parameterize methods like `queryGeneric` and 59 | * `mutationGeneric` to make them type-safe. 60 | */ 61 | export type DataModel = DataModelFromSchemaDefinition; 62 | -------------------------------------------------------------------------------- /convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.10.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | GenericActionCtx, 18 | GenericMutationCtx, 19 | GenericQueryCtx, 20 | GenericDatabaseReader, 21 | GenericDatabaseWriter, 22 | } from "convex/server"; 23 | import type { DataModel } from "./dataModel.js"; 24 | 25 | /** 26 | * Define a query in this Convex app's public API. 27 | * 28 | * This function will be allowed to read your Convex database and will be accessible from the client. 29 | * 30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 32 | */ 33 | export declare const query: QueryBuilder; 34 | 35 | /** 36 | * Define a query that is only accessible from other Convex functions (but not from the client). 37 | * 38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 39 | * 40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 42 | */ 43 | export declare const internalQuery: QueryBuilder; 44 | 45 | /** 46 | * Define a mutation in this Convex app's public API. 47 | * 48 | * This function will be allowed to modify your Convex database and will be accessible from the client. 49 | * 50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 52 | */ 53 | export declare const mutation: MutationBuilder; 54 | 55 | /** 56 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 57 | * 58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 59 | * 60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 62 | */ 63 | export declare const internalMutation: MutationBuilder; 64 | 65 | /** 66 | * Define an action in this Convex app's public API. 67 | * 68 | * An action is a function which can execute any JavaScript code, including non-deterministic 69 | * code and code with side-effects, like calling third-party services. 70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 72 | * 73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 75 | */ 76 | export declare const action: ActionBuilder; 77 | 78 | /** 79 | * Define an action that is only accessible from other Convex functions (but not from the client). 80 | * 81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 83 | */ 84 | export declare const internalAction: ActionBuilder; 85 | 86 | /** 87 | * Define an HTTP action. 88 | * 89 | * This function will be used to respond to HTTP requests received by a Convex 90 | * deployment if the requests matches the path and method where this action 91 | * is routed. Be sure to route your action in `convex/http.js`. 92 | * 93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 95 | */ 96 | export declare const httpAction: HttpActionBuilder; 97 | 98 | /** 99 | * A set of services for use within Convex query functions. 100 | * 101 | * The query context is passed as the first argument to any Convex query 102 | * function run on the server. 103 | * 104 | * This differs from the {@link MutationCtx} because all of the services are 105 | * read-only. 106 | */ 107 | export type QueryCtx = GenericQueryCtx; 108 | 109 | /** 110 | * A set of services for use within Convex mutation functions. 111 | * 112 | * The mutation context is passed as the first argument to any Convex mutation 113 | * function run on the server. 114 | */ 115 | export type MutationCtx = GenericMutationCtx; 116 | 117 | /** 118 | * A set of services for use within Convex action functions. 119 | * 120 | * The action context is passed as the first argument to any Convex action 121 | * function run on the server. 122 | */ 123 | export type ActionCtx = GenericActionCtx; 124 | 125 | /** 126 | * An interface to read from the database within Convex query functions. 127 | * 128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 130 | * building a query. 131 | */ 132 | export type DatabaseReader = GenericDatabaseReader; 133 | 134 | /** 135 | * An interface to read from and write to the database within Convex mutation 136 | * functions. 137 | * 138 | * Convex guarantees that all writes within a single mutation are 139 | * executed atomically, so you never have to worry about partial writes leaving 140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 141 | * for the guarantees Convex provides your functions. 142 | */ 143 | export type DatabaseWriter = GenericDatabaseWriter; 144 | -------------------------------------------------------------------------------- /convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.10.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | actionGeneric, 14 | httpActionGeneric, 15 | queryGeneric, 16 | mutationGeneric, 17 | internalActionGeneric, 18 | internalMutationGeneric, 19 | internalQueryGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /convex/auth.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | providers: [ 3 | { 4 | domain: "https://poetic-salmon-66.clerk.accounts.dev", 5 | applicationID: "convex", 6 | }, 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /convex/clerk.ts: -------------------------------------------------------------------------------- 1 | "use node"; 2 | 3 | import type { WebhookEvent } from "@clerk/clerk-sdk-node"; 4 | import { v } from "convex/values"; 5 | import { Webhook } from "svix"; 6 | 7 | import { internalAction } from "./_generated/server"; 8 | 9 | const webhookSecret = process.env.CLERK_WEBHOOK_SECRET || ``; 10 | 11 | export const fulfill = internalAction({ 12 | args: { headers: v.any(), payload: v.string() }, 13 | handler: async (ctx, args) => { 14 | const wh = new Webhook(webhookSecret); 15 | const payload = wh.verify(args.payload, args.headers) as WebhookEvent; 16 | return payload; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /convex/crons.ts: -------------------------------------------------------------------------------- 1 | import { cronJobs } from "convex/server"; 2 | import { internal } from "./_generated/api"; 3 | 4 | const crons = cronJobs(); 5 | 6 | crons.interval( 7 | "delete any old files marked for deletion", 8 | { minutes: 1 }, 9 | internal.files.deleteAllFiles 10 | ); 11 | 12 | export default crons; 13 | -------------------------------------------------------------------------------- /convex/files.ts: -------------------------------------------------------------------------------- 1 | import { ConvexError, v } from "convex/values"; 2 | import { 3 | MutationCtx, 4 | QueryCtx, 5 | internalMutation, 6 | mutation, 7 | query, 8 | } from "./_generated/server"; 9 | import { getUser } from "./users"; 10 | import { fileTypes } from "./schema"; 11 | import { Doc, Id } from "./_generated/dataModel"; 12 | 13 | export const generateUploadUrl = mutation(async (ctx) => { 14 | const identity = await ctx.auth.getUserIdentity(); 15 | 16 | if (!identity) { 17 | throw new ConvexError("you must be logged in to upload a file"); 18 | } 19 | 20 | return await ctx.storage.generateUploadUrl(); 21 | }); 22 | 23 | export async function hasAccessToOrg( 24 | ctx: QueryCtx | MutationCtx, 25 | orgId: string 26 | ) { 27 | const identity = await ctx.auth.getUserIdentity(); 28 | 29 | if (!identity) { 30 | return null; 31 | } 32 | 33 | const user = await ctx.db 34 | .query("users") 35 | .withIndex("by_tokenIdentifier", (q) => 36 | q.eq("tokenIdentifier", identity.tokenIdentifier) 37 | ) 38 | .first(); 39 | 40 | if (!user) { 41 | return null; 42 | } 43 | 44 | const hasAccess = 45 | user.orgIds.some((item) => item.orgId === orgId) || 46 | user.tokenIdentifier.includes(orgId); 47 | 48 | if (!hasAccess) { 49 | return null; 50 | } 51 | 52 | return { user }; 53 | } 54 | 55 | export const createFile = mutation({ 56 | args: { 57 | name: v.string(), 58 | fileId: v.id("_storage"), 59 | orgId: v.string(), 60 | type: fileTypes, 61 | }, 62 | async handler(ctx, args) { 63 | const hasAccess = await hasAccessToOrg(ctx, args.orgId); 64 | 65 | if (!hasAccess) { 66 | throw new ConvexError("you do not have access to this org"); 67 | } 68 | 69 | await ctx.db.insert("files", { 70 | name: args.name, 71 | orgId: args.orgId, 72 | fileId: args.fileId, 73 | type: args.type, 74 | userId: hasAccess.user._id, 75 | }); 76 | }, 77 | }); 78 | 79 | export const getFiles = query({ 80 | args: { 81 | orgId: v.string(), 82 | query: v.optional(v.string()), 83 | favorites: v.optional(v.boolean()), 84 | deletedOnly: v.optional(v.boolean()), 85 | type: v.optional(fileTypes), 86 | }, 87 | async handler(ctx, args) { 88 | const hasAccess = await hasAccessToOrg(ctx, args.orgId); 89 | 90 | if (!hasAccess) { 91 | return []; 92 | } 93 | 94 | let files = await ctx.db 95 | .query("files") 96 | .withIndex("by_orgId", (q) => q.eq("orgId", args.orgId)) 97 | .collect(); 98 | 99 | const query = args.query; 100 | 101 | if (query) { 102 | files = files.filter((file) => 103 | file.name.toLowerCase().includes(query.toLowerCase()) 104 | ); 105 | } 106 | 107 | if (args.favorites) { 108 | const favorites = await ctx.db 109 | .query("favorites") 110 | .withIndex("by_userId_orgId_fileId", (q) => 111 | q.eq("userId", hasAccess.user._id).eq("orgId", args.orgId) 112 | ) 113 | .collect(); 114 | 115 | files = files.filter((file) => 116 | favorites.some((favorite) => favorite.fileId === file._id) 117 | ); 118 | } 119 | 120 | if (args.deletedOnly) { 121 | files = files.filter((file) => file.shouldDelete); 122 | } else { 123 | files = files.filter((file) => !file.shouldDelete); 124 | } 125 | 126 | if (args.type) { 127 | files = files.filter((file) => file.type === args.type); 128 | } 129 | 130 | const filesWithUrl = await Promise.all( 131 | files.map(async (file) => ({ 132 | ...file, 133 | url: await ctx.storage.getUrl(file.fileId), 134 | })) 135 | ); 136 | 137 | return filesWithUrl; 138 | }, 139 | }); 140 | 141 | export const deleteAllFiles = internalMutation({ 142 | args: {}, 143 | async handler(ctx) { 144 | const files = await ctx.db 145 | .query("files") 146 | .withIndex("by_shouldDelete", (q) => q.eq("shouldDelete", true)) 147 | .collect(); 148 | 149 | await Promise.all( 150 | files.map(async (file) => { 151 | await ctx.storage.delete(file.fileId); 152 | return await ctx.db.delete(file._id); 153 | }) 154 | ); 155 | }, 156 | }); 157 | 158 | function assertCanDeleteFile(user: Doc<"users">, file: Doc<"files">) { 159 | const canDelete = 160 | file.userId === user._id || 161 | user.orgIds.find((org) => org.orgId === file.orgId)?.role === "admin"; 162 | 163 | if (!canDelete) { 164 | throw new ConvexError("you have no acces to delete this file"); 165 | } 166 | } 167 | 168 | export const deleteFile = mutation({ 169 | args: { fileId: v.id("files") }, 170 | async handler(ctx, args) { 171 | const access = await hasAccessToFile(ctx, args.fileId); 172 | 173 | if (!access) { 174 | throw new ConvexError("no access to file"); 175 | } 176 | 177 | assertCanDeleteFile(access.user, access.file); 178 | 179 | await ctx.db.patch(args.fileId, { 180 | shouldDelete: true, 181 | }); 182 | }, 183 | }); 184 | 185 | export const restoreFile = mutation({ 186 | args: { fileId: v.id("files") }, 187 | async handler(ctx, args) { 188 | const access = await hasAccessToFile(ctx, args.fileId); 189 | 190 | if (!access) { 191 | throw new ConvexError("no access to file"); 192 | } 193 | 194 | assertCanDeleteFile(access.user, access.file); 195 | 196 | await ctx.db.patch(args.fileId, { 197 | shouldDelete: false, 198 | }); 199 | }, 200 | }); 201 | 202 | export const toggleFavorite = mutation({ 203 | args: { fileId: v.id("files") }, 204 | async handler(ctx, args) { 205 | const access = await hasAccessToFile(ctx, args.fileId); 206 | 207 | if (!access) { 208 | throw new ConvexError("no access to file"); 209 | } 210 | 211 | const favorite = await ctx.db 212 | .query("favorites") 213 | .withIndex("by_userId_orgId_fileId", (q) => 214 | q 215 | .eq("userId", access.user._id) 216 | .eq("orgId", access.file.orgId) 217 | .eq("fileId", access.file._id) 218 | ) 219 | .first(); 220 | 221 | if (!favorite) { 222 | await ctx.db.insert("favorites", { 223 | fileId: access.file._id, 224 | userId: access.user._id, 225 | orgId: access.file.orgId, 226 | }); 227 | } else { 228 | await ctx.db.delete(favorite._id); 229 | } 230 | }, 231 | }); 232 | 233 | export const getAllFavorites = query({ 234 | args: { orgId: v.string() }, 235 | async handler(ctx, args) { 236 | const hasAccess = await hasAccessToOrg(ctx, args.orgId); 237 | 238 | if (!hasAccess) { 239 | return []; 240 | } 241 | 242 | const favorites = await ctx.db 243 | .query("favorites") 244 | .withIndex("by_userId_orgId_fileId", (q) => 245 | q.eq("userId", hasAccess.user._id).eq("orgId", args.orgId) 246 | ) 247 | .collect(); 248 | 249 | return favorites; 250 | }, 251 | }); 252 | 253 | async function hasAccessToFile( 254 | ctx: QueryCtx | MutationCtx, 255 | fileId: Id<"files"> 256 | ) { 257 | const file = await ctx.db.get(fileId); 258 | 259 | if (!file) { 260 | return null; 261 | } 262 | 263 | const hasAccess = await hasAccessToOrg(ctx, file.orgId); 264 | 265 | if (!hasAccess) { 266 | return null; 267 | } 268 | 269 | return { user: hasAccess.user, file }; 270 | } 271 | -------------------------------------------------------------------------------- /convex/http.ts: -------------------------------------------------------------------------------- 1 | import { httpRouter } from "convex/server"; 2 | 3 | import { internal } from "./_generated/api"; 4 | import { httpAction } from "./_generated/server"; 5 | 6 | const http = httpRouter(); 7 | 8 | http.route({ 9 | path: "/clerk", 10 | method: "POST", 11 | handler: httpAction(async (ctx, request) => { 12 | const payloadString = await request.text(); 13 | const headerPayload = request.headers; 14 | 15 | try { 16 | const result = await ctx.runAction(internal.clerk.fulfill, { 17 | payload: payloadString, 18 | headers: { 19 | "svix-id": headerPayload.get("svix-id")!, 20 | "svix-timestamp": headerPayload.get("svix-timestamp")!, 21 | "svix-signature": headerPayload.get("svix-signature")!, 22 | }, 23 | }); 24 | 25 | switch (result.type) { 26 | case "user.created": 27 | await ctx.runMutation(internal.users.createUser, { 28 | tokenIdentifier: `https://${process.env.CLERK_HOSTNAME}|${result.data.id}`, 29 | name: `${result.data.first_name ?? ""} ${ 30 | result.data.last_name ?? "" 31 | }`, 32 | image: result.data.image_url, 33 | }); 34 | break; 35 | case "user.updated": 36 | await ctx.runMutation(internal.users.updateUser, { 37 | tokenIdentifier: `https://${process.env.CLERK_HOSTNAME}|${result.data.id}`, 38 | name: `${result.data.first_name ?? ""} ${ 39 | result.data.last_name ?? "" 40 | }`, 41 | image: result.data.image_url, 42 | }); 43 | break; 44 | case "organizationMembership.created": 45 | await ctx.runMutation(internal.users.addOrgIdToUser, { 46 | tokenIdentifier: `https://${process.env.CLERK_HOSTNAME}|${result.data.public_user_data.user_id}`, 47 | orgId: result.data.organization.id, 48 | role: result.data.role === "org:admin" ? "admin" : "member", 49 | }); 50 | break; 51 | case "organizationMembership.updated": 52 | console.log(result.data.role); 53 | await ctx.runMutation(internal.users.updateRoleInOrgForUser, { 54 | tokenIdentifier: `https://${process.env.CLERK_HOSTNAME}|${result.data.public_user_data.user_id}`, 55 | orgId: result.data.organization.id, 56 | role: result.data.role === "org:admin" ? "admin" : "member", 57 | }); 58 | break; 59 | } 60 | 61 | return new Response(null, { 62 | status: 200, 63 | }); 64 | } catch (err) { 65 | return new Response("Webhook Error", { 66 | status: 400, 67 | }); 68 | } 69 | }), 70 | }); 71 | 72 | export default http; 73 | -------------------------------------------------------------------------------- /convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from "convex/server"; 2 | import { v } from "convex/values"; 3 | 4 | export const fileTypes = v.union( 5 | v.literal("image"), 6 | v.literal("csv"), 7 | v.literal("pdf") 8 | ); 9 | 10 | export const roles = v.union(v.literal("admin"), v.literal("member")); 11 | 12 | export default defineSchema({ 13 | files: defineTable({ 14 | name: v.string(), 15 | type: fileTypes, 16 | orgId: v.string(), 17 | fileId: v.id("_storage"), 18 | userId: v.id("users"), 19 | shouldDelete: v.optional(v.boolean()), 20 | }) 21 | .index("by_orgId", ["orgId"]) 22 | .index("by_shouldDelete", ["shouldDelete"]), 23 | favorites: defineTable({ 24 | fileId: v.id("files"), 25 | orgId: v.string(), 26 | userId: v.id("users"), 27 | }).index("by_userId_orgId_fileId", ["userId", "orgId", "fileId"]), 28 | users: defineTable({ 29 | tokenIdentifier: v.string(), 30 | name: v.optional(v.string()), 31 | image: v.optional(v.string()), 32 | orgIds: v.array( 33 | v.object({ 34 | orgId: v.string(), 35 | role: roles, 36 | }) 37 | ), 38 | }).index("by_tokenIdentifier", ["tokenIdentifier"]), 39 | }); 40 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "skipLibCheck": true, 20 | "noEmit": true 21 | }, 22 | "include": ["./**/*"], 23 | "exclude": ["./_generated"] 24 | } 25 | -------------------------------------------------------------------------------- /convex/users.ts: -------------------------------------------------------------------------------- 1 | import { ConvexError, v } from "convex/values"; 2 | import { 3 | MutationCtx, 4 | QueryCtx, 5 | internalMutation, 6 | query, 7 | } from "./_generated/server"; 8 | import { roles } from "./schema"; 9 | import { hasAccessToOrg } from "./files"; 10 | 11 | export async function getUser( 12 | ctx: QueryCtx | MutationCtx, 13 | tokenIdentifier: string 14 | ) { 15 | const user = await ctx.db 16 | .query("users") 17 | .withIndex("by_tokenIdentifier", (q) => 18 | q.eq("tokenIdentifier", tokenIdentifier) 19 | ) 20 | .first(); 21 | 22 | if (!user) { 23 | throw new ConvexError("expected user to be defined"); 24 | } 25 | 26 | return user; 27 | } 28 | 29 | export const createUser = internalMutation({ 30 | args: { tokenIdentifier: v.string(), name: v.string(), image: v.string() }, 31 | async handler(ctx, args) { 32 | await ctx.db.insert("users", { 33 | tokenIdentifier: args.tokenIdentifier, 34 | orgIds: [], 35 | name: args.name, 36 | image: args.image, 37 | }); 38 | }, 39 | }); 40 | 41 | export const updateUser = internalMutation({ 42 | args: { tokenIdentifier: v.string(), name: v.string(), image: v.string() }, 43 | async handler(ctx, args) { 44 | const user = await ctx.db 45 | .query("users") 46 | .withIndex("by_tokenIdentifier", (q) => 47 | q.eq("tokenIdentifier", args.tokenIdentifier) 48 | ) 49 | .first(); 50 | 51 | if (!user) { 52 | throw new ConvexError("no user with this token found"); 53 | } 54 | 55 | await ctx.db.patch(user._id, { 56 | name: args.name, 57 | image: args.image, 58 | }); 59 | }, 60 | }); 61 | 62 | export const addOrgIdToUser = internalMutation({ 63 | args: { tokenIdentifier: v.string(), orgId: v.string(), role: roles }, 64 | async handler(ctx, args) { 65 | const user = await getUser(ctx, args.tokenIdentifier); 66 | 67 | await ctx.db.patch(user._id, { 68 | orgIds: [...user.orgIds, { orgId: args.orgId, role: args.role }], 69 | }); 70 | }, 71 | }); 72 | 73 | export const updateRoleInOrgForUser = internalMutation({ 74 | args: { tokenIdentifier: v.string(), orgId: v.string(), role: roles }, 75 | async handler(ctx, args) { 76 | const user = await getUser(ctx, args.tokenIdentifier); 77 | 78 | const org = user.orgIds.find((org) => org.orgId === args.orgId); 79 | 80 | if (!org) { 81 | throw new ConvexError( 82 | "expected an org on the user but was not found when updating" 83 | ); 84 | } 85 | 86 | org.role = args.role; 87 | 88 | await ctx.db.patch(user._id, { 89 | orgIds: user.orgIds, 90 | }); 91 | }, 92 | }); 93 | 94 | export const getUserProfile = query({ 95 | args: { userId: v.id("users") }, 96 | async handler(ctx, args) { 97 | const user = await ctx.db.get(args.userId); 98 | 99 | return { 100 | name: user?.name, 101 | image: user?.image, 102 | }; 103 | }, 104 | }); 105 | 106 | export const getMe = query({ 107 | args: {}, 108 | async handler(ctx) { 109 | const identity = await ctx.auth.getUserIdentity(); 110 | 111 | if (!identity) { 112 | return null; 113 | } 114 | 115 | const user = await getUser(ctx, identity.tokenIdentifier); 116 | 117 | if (!user) { 118 | return null; 119 | } 120 | 121 | return user; 122 | }, 123 | }); 124 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | hostname: "adventurous-caiman-790.convex.cloud", 7 | }, 8 | ], 9 | }, 10 | }; 11 | 12 | export default nextConfig; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-drive", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/clerk-sdk-node": "^4.13.9", 13 | "@clerk/nextjs": "^4.29.7", 14 | "@hookform/resolvers": "^3.3.4", 15 | "@radix-ui/react-alert-dialog": "^1.0.5", 16 | "@radix-ui/react-avatar": "^1.0.4", 17 | "@radix-ui/react-dialog": "^1.0.5", 18 | "@radix-ui/react-dropdown-menu": "^2.0.6", 19 | "@radix-ui/react-label": "^2.0.2", 20 | "@radix-ui/react-select": "^2.0.0", 21 | "@radix-ui/react-slot": "^1.0.2", 22 | "@radix-ui/react-tabs": "^1.0.4", 23 | "@radix-ui/react-toast": "^1.1.5", 24 | "@tanstack/react-table": "^8.12.0", 25 | "class-variance-authority": "^0.7.0", 26 | "clsx": "^2.1.0", 27 | "convex": "^1.9.1", 28 | "date-fns": "^3.3.1", 29 | "lucide-react": "^0.336.0", 30 | "next": "14.1.0", 31 | "react": "^18", 32 | "react-dom": "^18", 33 | "react-hook-form": "^7.50.1", 34 | "svix": "^1.19.0", 35 | "tailwind-merge": "^2.2.1", 36 | "tailwindcss-animate": "^1.0.7", 37 | "zod": "^3.22.4" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "^20", 41 | "@types/react": "^18", 42 | "@types/react-dom": "^18", 43 | "autoprefixer": "^10.0.1", 44 | "eslint": "^8", 45 | "eslint-config-next": "14.1.0", 46 | "postcss": "^8", 47 | "tailwindcss": "^3.3.0", 48 | "typescript": "^5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/empty.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/file-drive/0e31fcd44134d65ab95d4de95f3454f1383871b0/public/logo.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/ConvexClientProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | import { ConvexReactClient } from "convex/react"; 5 | import { ClerkProvider, useAuth } from "@clerk/nextjs"; 6 | import { ConvexProviderWithClerk } from "convex/react-clerk"; 7 | 8 | const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); 9 | 10 | export default function ConvexClientProvider({ 11 | children, 12 | }: { 13 | children: ReactNode; 14 | }) { 15 | return ( 16 | 19 | 20 | {children} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/dashboard/_components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | import { Doc, Id } from "../../../../convex/_generated/dataModel"; 5 | import { formatRelative } from "date-fns"; 6 | import { useQuery } from "convex/react"; 7 | import { api } from "../../../../convex/_generated/api"; 8 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 9 | import { FileCardActions } from "./file-actions"; 10 | 11 | function UserCell({ userId }: { userId: Id<"users"> }) { 12 | const userProfile = useQuery(api.users.getUserProfile, { 13 | userId: userId, 14 | }); 15 | return ( 16 |
17 | 18 | 19 | CN 20 | 21 | {userProfile?.name} 22 |
23 | ); 24 | } 25 | 26 | export const columns: ColumnDef< 27 | Doc<"files"> & { url: string; isFavorited: boolean } 28 | >[] = [ 29 | { 30 | accessorKey: "name", 31 | header: "Name", 32 | }, 33 | { 34 | accessorKey: "type", 35 | header: "Type", 36 | }, 37 | { 38 | header: "User", 39 | cell: ({ row }) => { 40 | return ; 41 | }, 42 | }, 43 | { 44 | header: "Uploaded On", 45 | cell: ({ row }) => { 46 | return ( 47 |
48 | {formatRelative(new Date(row.original._creationTime), new Date())} 49 |
50 | ); 51 | }, 52 | }, 53 | { 54 | header: "Actions", 55 | cell: ({ row }) => { 56 | return ( 57 |
58 | 62 |
63 | ); 64 | }, 65 | }, 66 | ]; 67 | -------------------------------------------------------------------------------- /src/app/dashboard/_components/file-actions.tsx: -------------------------------------------------------------------------------- 1 | import { Doc, Id } from "../../../../convex/_generated/dataModel"; 2 | import { 3 | DropdownMenu, 4 | DropdownMenuContent, 5 | DropdownMenuItem, 6 | DropdownMenuSeparator, 7 | DropdownMenuTrigger, 8 | } from "@/components/ui/dropdown-menu"; 9 | import { 10 | FileIcon, 11 | MoreVertical, 12 | StarHalf, 13 | StarIcon, 14 | TrashIcon, 15 | UndoIcon, 16 | } from "lucide-react"; 17 | import { 18 | AlertDialog, 19 | AlertDialogAction, 20 | AlertDialogCancel, 21 | AlertDialogContent, 22 | AlertDialogDescription, 23 | AlertDialogFooter, 24 | AlertDialogHeader, 25 | AlertDialogTitle, 26 | } from "@/components/ui/alert-dialog"; 27 | import { useState } from "react"; 28 | import { useMutation, useQuery } from "convex/react"; 29 | import { api } from "../../../../convex/_generated/api"; 30 | import { useToast } from "@/components/ui/use-toast"; 31 | import { Protect } from "@clerk/nextjs"; 32 | 33 | export function FileCardActions({ 34 | file, 35 | isFavorited, 36 | }: { 37 | file: Doc<"files"> & { url: string | null }; 38 | isFavorited: boolean; 39 | }) { 40 | const deleteFile = useMutation(api.files.deleteFile); 41 | const restoreFile = useMutation(api.files.restoreFile); 42 | const toggleFavorite = useMutation(api.files.toggleFavorite); 43 | const { toast } = useToast(); 44 | const me = useQuery(api.users.getMe); 45 | 46 | const [isConfirmOpen, setIsConfirmOpen] = useState(false); 47 | 48 | return ( 49 | <> 50 | 51 | 52 | 53 | Are you absolutely sure? 54 | 55 | This action will mark the file for our deletion process. Files are 56 | deleted periodically 57 | 58 | 59 | 60 | Cancel 61 | { 63 | await deleteFile({ 64 | fileId: file._id, 65 | }); 66 | toast({ 67 | variant: "default", 68 | title: "File marked for deletion", 69 | description: "Your file will be deleted soon", 70 | }); 71 | }} 72 | > 73 | Continue 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | { 86 | if (!file.url) return; 87 | window.open(file.url, "_blank"); 88 | }} 89 | className="flex gap-1 items-center cursor-pointer" 90 | > 91 | Download 92 | 93 | 94 | { 96 | toggleFavorite({ 97 | fileId: file._id, 98 | }); 99 | }} 100 | className="flex gap-1 items-center cursor-pointer" 101 | > 102 | {isFavorited ? ( 103 |
104 | Unfavorite 105 |
106 | ) : ( 107 |
108 | Favorite 109 |
110 | )} 111 |
112 | 113 | { 115 | return ( 116 | check({ 117 | role: "org:admin", 118 | }) || file.userId === me?._id 119 | ); 120 | }} 121 | fallback={<>} 122 | > 123 | 124 | { 126 | if (file.shouldDelete) { 127 | restoreFile({ 128 | fileId: file._id, 129 | }); 130 | } else { 131 | setIsConfirmOpen(true); 132 | } 133 | }} 134 | className="flex gap-1 items-center cursor-pointer" 135 | > 136 | {file.shouldDelete ? ( 137 |
138 | Restore 139 |
140 | ) : ( 141 |
142 | Delete 143 |
144 | )} 145 |
146 |
147 |
148 |
149 | 150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /src/app/dashboard/_components/file-browser.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useOrganization, useUser } from "@clerk/nextjs"; 3 | import { useQuery } from "convex/react"; 4 | import { api } from "../../../../convex/_generated/api"; 5 | import { UploadButton } from "./upload-button"; 6 | import { FileCard } from "./file-card"; 7 | import Image from "next/image"; 8 | import { GridIcon, Loader2, RowsIcon } from "lucide-react"; 9 | import { SearchBar } from "./search-bar"; 10 | import { useState } from "react"; 11 | import { DataTable } from "./file-table"; 12 | import { columns } from "./columns"; 13 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 14 | import { 15 | Select, 16 | SelectContent, 17 | SelectItem, 18 | SelectTrigger, 19 | SelectValue, 20 | } from "@/components/ui/select"; 21 | import { Doc } from "../../../../convex/_generated/dataModel"; 22 | import { Label } from "@/components/ui/label"; 23 | 24 | function Placeholder() { 25 | return ( 26 |
27 | an image of a picture and directory icon 33 |
You have no files, upload one now
34 | 35 |
36 | ); 37 | } 38 | 39 | export function FileBrowser({ 40 | title, 41 | favoritesOnly, 42 | deletedOnly, 43 | }: { 44 | title: string; 45 | favoritesOnly?: boolean; 46 | deletedOnly?: boolean; 47 | }) { 48 | const organization = useOrganization(); 49 | const user = useUser(); 50 | const [query, setQuery] = useState(""); 51 | const [type, setType] = useState["type"] | "all">("all"); 52 | 53 | let orgId: string | undefined = undefined; 54 | if (organization.isLoaded && user.isLoaded) { 55 | orgId = organization.organization?.id ?? user.user?.id; 56 | } 57 | 58 | const favorites = useQuery( 59 | api.files.getAllFavorites, 60 | orgId ? { orgId } : "skip" 61 | ); 62 | 63 | const files = useQuery( 64 | api.files.getFiles, 65 | orgId 66 | ? { 67 | orgId, 68 | type: type === "all" ? undefined : type, 69 | query, 70 | favorites: favoritesOnly, 71 | deletedOnly, 72 | } 73 | : "skip" 74 | ); 75 | const isLoading = files === undefined; 76 | 77 | const modifiedFiles = 78 | files?.map((file) => ({ 79 | ...file, 80 | isFavorited: (favorites ?? []).some( 81 | (favorite) => favorite.fileId === file._id 82 | ), 83 | })) ?? []; 84 | 85 | return ( 86 |
87 |
88 |

{title}

89 | 90 | 91 | 92 | 93 |
94 | 95 | 96 |
97 | 98 | 99 | 100 | Grid 101 | 102 | 103 | Table 104 | 105 | 106 | 107 |
108 | 109 | 125 |
126 |
127 | 128 | {isLoading && ( 129 |
130 | 131 |
Loading your files...
132 |
133 | )} 134 | 135 | 136 |
137 | {modifiedFiles?.map((file) => { 138 | return ; 139 | })} 140 |
141 |
142 | 143 | 144 | 145 |
146 | 147 | {files?.length === 0 && } 148 |
149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /src/app/dashboard/_components/file-card.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardFooter, 5 | CardHeader, 6 | CardTitle, 7 | } from "@/components/ui/card"; 8 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 9 | import { formatRelative } from "date-fns"; 10 | 11 | import { Doc } from "../../../../convex/_generated/dataModel"; 12 | import { FileTextIcon, GanttChartIcon, ImageIcon } from "lucide-react"; 13 | import { ReactNode } from "react"; 14 | import { useQuery } from "convex/react"; 15 | import { api } from "../../../../convex/_generated/api"; 16 | import Image from "next/image"; 17 | import { FileCardActions } from "./file-actions"; 18 | 19 | export function FileCard({ 20 | file, 21 | }: { 22 | file: Doc<"files"> & { isFavorited: boolean; url: string | null }; 23 | }) { 24 | const userProfile = useQuery(api.users.getUserProfile, { 25 | userId: file.userId, 26 | }); 27 | 28 | const typeIcons = { 29 | image: , 30 | pdf: , 31 | csv: , 32 | } as Record["type"], ReactNode>; 33 | 34 | return ( 35 | 36 | 37 | 38 |
{typeIcons[file.type]}
{" "} 39 | {file.name} 40 |
41 |
42 | 43 |
44 |
45 | 46 | {file.type === "image" && file.url && ( 47 | {file.name} 48 | )} 49 | 50 | {file.type === "csv" && } 51 | {file.type === "pdf" && } 52 | 53 | 54 |
55 | 56 | 57 | CN 58 | 59 | {userProfile?.name} 60 |
61 |
62 | Uploaded on {formatRelative(new Date(file._creationTime), new Date())} 63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/app/dashboard/_components/file-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ColumnDef, 5 | flexRender, 6 | getCoreRowModel, 7 | useReactTable, 8 | } from "@tanstack/react-table"; 9 | 10 | import { 11 | Table, 12 | TableBody, 13 | TableCell, 14 | TableHead, 15 | TableHeader, 16 | TableRow, 17 | } from "@/components/ui/table"; 18 | 19 | interface DataTableProps { 20 | columns: ColumnDef[]; 21 | data: TData[]; 22 | } 23 | 24 | export function DataTable({ 25 | columns, 26 | data, 27 | }: DataTableProps) { 28 | const table = useReactTable({ 29 | data, 30 | columns, 31 | getCoreRowModel: getCoreRowModel(), 32 | }); 33 | 34 | return ( 35 |
36 | 37 | 38 | {table.getHeaderGroups().map((headerGroup) => ( 39 | 40 | {headerGroup.headers.map((header) => { 41 | return ( 42 | 43 | {header.isPlaceholder 44 | ? null 45 | : flexRender( 46 | header.column.columnDef.header, 47 | header.getContext() 48 | )} 49 | 50 | ); 51 | })} 52 | 53 | ))} 54 | 55 | 56 | {table.getRowModel().rows?.length ? ( 57 | table.getRowModel().rows.map((row) => ( 58 | 62 | {row.getVisibleCells().map((cell) => ( 63 | 64 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 65 | 66 | ))} 67 | 68 | )) 69 | ) : ( 70 | 71 | 72 | No results. 73 | 74 | 75 | )} 76 | 77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/app/dashboard/_components/search-bar.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Form, 4 | FormControl, 5 | FormField, 6 | FormItem, 7 | FormLabel, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { Input } from "@/components/ui/input"; 11 | import { zodResolver } from "@hookform/resolvers/zod"; 12 | import { Loader2, SearchIcon } from "lucide-react"; 13 | import { Dispatch, SetStateAction } from "react"; 14 | import { useForm } from "react-hook-form"; 15 | import { z } from "zod"; 16 | 17 | const formSchema = z.object({ 18 | query: z.string().min(0).max(200), 19 | }); 20 | 21 | export function SearchBar({ 22 | query, 23 | setQuery, 24 | }: { 25 | query: string; 26 | setQuery: Dispatch>; 27 | }) { 28 | const form = useForm>({ 29 | resolver: zodResolver(formSchema), 30 | defaultValues: { 31 | query, 32 | }, 33 | }); 34 | 35 | async function onSubmit(values: z.infer) { 36 | setQuery(values.query); 37 | } 38 | 39 | return ( 40 |
41 |
42 | 46 | ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | )} 57 | /> 58 | 59 | 70 | 71 | 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/app/dashboard/_components/upload-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { useOrganization, useUser } from "@clerk/nextjs"; 5 | import { 6 | Form, 7 | FormControl, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | } from "@/components/ui/form"; 13 | import { Input } from "@/components/ui/input"; 14 | import { useMutation } from "convex/react"; 15 | import { api } from "../../../../convex/_generated/api"; 16 | import { 17 | Dialog, 18 | DialogContent, 19 | DialogDescription, 20 | DialogHeader, 21 | DialogTitle, 22 | DialogTrigger, 23 | } from "@/components/ui/dialog"; 24 | 25 | import { z } from "zod"; 26 | 27 | import { zodResolver } from "@hookform/resolvers/zod"; 28 | import { useForm } from "react-hook-form"; 29 | import { useState } from "react"; 30 | import { useToast } from "@/components/ui/use-toast"; 31 | import { Loader2 } from "lucide-react"; 32 | import { Doc } from "../../../../convex/_generated/dataModel"; 33 | 34 | const formSchema = z.object({ 35 | title: z.string().min(1).max(200), 36 | file: z 37 | .custom((val) => val instanceof FileList, "Required") 38 | .refine((files) => files.length > 0, `Required`), 39 | }); 40 | 41 | export function UploadButton() { 42 | const { toast } = useToast(); 43 | const organization = useOrganization(); 44 | const user = useUser(); 45 | const generateUploadUrl = useMutation(api.files.generateUploadUrl); 46 | 47 | const form = useForm>({ 48 | resolver: zodResolver(formSchema), 49 | defaultValues: { 50 | title: "", 51 | file: undefined, 52 | }, 53 | }); 54 | 55 | const fileRef = form.register("file"); 56 | 57 | async function onSubmit(values: z.infer) { 58 | if (!orgId) return; 59 | 60 | const postUrl = await generateUploadUrl(); 61 | 62 | const fileType = values.file[0].type; 63 | 64 | const result = await fetch(postUrl, { 65 | method: "POST", 66 | headers: { "Content-Type": fileType }, 67 | body: values.file[0], 68 | }); 69 | const { storageId } = await result.json(); 70 | 71 | const types = { 72 | "image/png": "image", 73 | "application/pdf": "pdf", 74 | "text/csv": "csv", 75 | } as Record["type"]>; 76 | 77 | try { 78 | await createFile({ 79 | name: values.title, 80 | fileId: storageId, 81 | orgId, 82 | type: types[fileType], 83 | }); 84 | 85 | form.reset(); 86 | 87 | setIsFileDialogOpen(false); 88 | 89 | toast({ 90 | variant: "success", 91 | title: "File Uploaded", 92 | description: "Now everyone can view your file", 93 | }); 94 | } catch (err) { 95 | toast({ 96 | variant: "destructive", 97 | title: "Something went wrong", 98 | description: "Your file could not be uploaded, try again later", 99 | }); 100 | } 101 | } 102 | 103 | let orgId: string | undefined = undefined; 104 | if (organization.isLoaded && user.isLoaded) { 105 | orgId = organization.organization?.id ?? user.user?.id; 106 | } 107 | 108 | const [isFileDialogOpen, setIsFileDialogOpen] = useState(false); 109 | 110 | const createFile = useMutation(api.files.createFile); 111 | 112 | return ( 113 | { 116 | setIsFileDialogOpen(isOpen); 117 | form.reset(); 118 | }} 119 | > 120 | 121 | 122 | 123 | 124 | 125 | Upload your File Here 126 | 127 | This file will be accessible by anyone in your organization 128 | 129 | 130 | 131 |
132 |
133 | 134 | ( 138 | 139 | Title 140 | 141 | 142 | 143 | 144 | 145 | )} 146 | /> 147 | 148 | ( 152 | 153 | File 154 | 155 | 156 | 157 | 158 | 159 | )} 160 | /> 161 | 171 | 172 | 173 |
174 |
175 |
176 | ); 177 | } 178 | -------------------------------------------------------------------------------- /src/app/dashboard/favorites/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useQuery } from "convex/react"; 4 | import { FileBrowser } from "../_components/file-browser"; 5 | import { api } from "../../../../convex/_generated/api"; 6 | 7 | export default function FavoritesPage() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/dashboard/files/page.tsx: -------------------------------------------------------------------------------- 1 | import { FileBrowser } from "../_components/file-browser"; 2 | 3 | export default function FilesPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | // export const metadata: Metadata = { 2 | // title: "Create Next App", 3 | // description: "Generated by create next app", 4 | // }; 5 | 6 | import { SideNav } from "./side-nav"; 7 | 8 | export default function DashboardLayout({ 9 | children, 10 | }: Readonly<{ 11 | children: React.ReactNode; 12 | }>) { 13 | return ( 14 |
15 |
16 | 17 | 18 |
{children}
19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/dashboard/side-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import clsx from "clsx"; 5 | import { FileIcon, StarIcon, TrashIcon } from "lucide-react"; 6 | import Link from "next/link"; 7 | import { usePathname } from "next/navigation"; 8 | 9 | export function SideNav() { 10 | const pathname = usePathname(); 11 | 12 | return ( 13 |
14 | 15 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | 37 | 45 | 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/dashboard/trash/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FileBrowser } from "../_components/file-browser"; 4 | 5 | export default function FavoritesPage() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevcody/file-drive/0e31fcd44134d65ab95d4de95f3454f1383871b0/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export function Footer() { 4 | return ( 5 |
6 |
7 |
FileDrive
8 | 9 | 10 | Privacy Policy 11 | 12 | 16 | Terms of Service 17 | 18 | 19 | About 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --success: 88 77% 78%; 29 | --success-foreground: 0 100% 0%; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 210 40% 98%; 33 | 34 | --border: 214.3 31.8% 91.4%; 35 | --input: 214.3 31.8% 91.4%; 36 | --ring: 222.2 84% 4.9%; 37 | 38 | --radius: 0.5rem; 39 | } 40 | 41 | .dark { 42 | --background: 222.2 84% 4.9%; 43 | --foreground: 210 40% 98%; 44 | 45 | --card: 222.2 84% 4.9%; 46 | --card-foreground: 210 40% 98%; 47 | 48 | --popover: 222.2 84% 4.9%; 49 | --popover-foreground: 210 40% 98%; 50 | 51 | --primary: 210 40% 98%; 52 | --primary-foreground: 222.2 47.4% 11.2%; 53 | 54 | --secondary: 217.2 32.6% 17.5%; 55 | --secondary-foreground: 210 40% 98%; 56 | 57 | --muted: 217.2 32.6% 17.5%; 58 | --muted-foreground: 215 20.2% 65.1%; 59 | 60 | --accent: 217.2 32.6% 17.5%; 61 | --accent-foreground: 210 40% 98%; 62 | 63 | --destructive: 0 62.8% 30.6%; 64 | --destructive-foreground: 210 40% 98%; 65 | 66 | --border: 217.2 32.6% 17.5%; 67 | --input: 217.2 32.6% 17.5%; 68 | --ring: 212.7 26.8% 83.9%; 69 | } 70 | } 71 | 72 | @layer base { 73 | * { 74 | @apply border-border; 75 | } 76 | body { 77 | @apply bg-background text-foreground; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/app/header.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | OrganizationSwitcher, 4 | SignInButton, 5 | SignedIn, 6 | SignedOut, 7 | UserButton, 8 | useSession, 9 | } from "@clerk/nextjs"; 10 | import Image from "next/image"; 11 | import Link from "next/link"; 12 | 13 | export function Header() { 14 | return ( 15 |
16 |
17 | 18 | file drive logo 19 | FileDrive 20 | 21 | 22 | 23 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import ConvexClientProvider from "./ConvexClientProvider"; 5 | import { Header } from "./header"; 6 | import { Toaster } from "@/components/ui/toaster"; 7 | import { Footer } from "./footer"; 8 | 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | export const metadata: Metadata = { 12 | title: "Create Next App", 13 | description: "Generated by create next app", 14 | }; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: Readonly<{ 19 | children: React.ReactNode; 20 | }>) { 21 | return ( 22 | 23 | 24 | 25 | 26 |
27 | {children} 28 |