├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── (main) │ ├── (routes) │ │ └── documents │ │ │ ├── [documentId] │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── _components │ │ ├── Banner.tsx │ │ ├── DocumentList.tsx │ │ ├── Item.tsx │ │ ├── Menu.tsx │ │ ├── Navbar.tsx │ │ ├── Navigation.tsx │ │ ├── Publish.tsx │ │ ├── Title.tsx │ │ ├── TrashBox.tsx │ │ └── UserItem.tsx │ └── layout.tsx ├── (marketing) │ ├── _components │ │ ├── FAQs.tsx │ │ ├── Features.tsx │ │ ├── Footer.tsx │ │ ├── Heading.tsx │ │ ├── Heroes.tsx │ │ ├── Logo.tsx │ │ ├── Navbar.tsx │ │ └── WorkTogether.tsx │ ├── layout.tsx │ └── page.tsx ├── (public) │ ├── (routes) │ │ └── preview │ │ │ └── [documentId] │ │ │ └── page.tsx │ └── layout.tsx ├── api │ └── edgestore │ │ └── [...edgestore] │ │ └── route.ts ├── error.tsx ├── globals.css └── layout.tsx ├── components.json ├── components ├── cover.tsx ├── editor.tsx ├── icon-picker.tsx ├── modals │ ├── confirm-modal.tsx │ ├── cover-image-modal.tsx │ └── settings-modal.tsx ├── mode-toggle.tsx ├── providers │ ├── convex-provider.tsx │ ├── modal-provider.tsx │ └── theme-provider.tsx ├── search-command.tsx ├── single-image-dropzone.tsx ├── spinner.tsx ├── toolbar.tsx └── ui │ ├── alert-dialog.tsx │ ├── avatar.tsx │ ├── button.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ └── skeleton.tsx ├── convex ├── README.md ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js ├── auth.config.js ├── documents.ts ├── schema.ts └── tsconfig.json ├── hooks ├── use-cover-image.tsx ├── use-origin.tsx ├── use-scroll-top.tsx ├── use-search.tsx └── use-settings.tsx ├── image-2.png ├── image-3.png ├── image-4.png ├── image.png ├── lib ├── edgestore.ts └── utils.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── empty-dark.png ├── empty.png ├── error-dark.png ├── error.png ├── logo-dark.jpg ├── logo.jpg ├── next.svg ├── notes-pana-dark.png ├── notes-pana.png └── vercel.svg ├── 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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quickflow: A Better & Open-Source Alternative to Notion 2 | 3 | ![Image of the app here](image.png) 4 | 5 | Quickflow is an open-source alternative to Notion that aims to provide a superior user experience. 6 | 7 | ## Technologies Used 8 | 9 | - [Next.js 13](https://nextjs.org/blog/next-13): A powerful React framework for building modern web applications. 10 | - [Shadcn UI](https://ui.shadcn.com/): Beautifully designed components that you can copy and paste into your apps. 11 | - [Clerk](https://clerk.com/): A complete suite of embeddable UIs, flexible APIs, and admin dashboards to authenticate and manage your users. 12 | - [Convex](https://www.convex.dev/): A Backend Application Platform that keeps you focused on building your product. Convex Functions, Database, File Storage, Scheduling, and Search fit together cohesively. 13 | - [TailwindCSS](https://tailwindcss.com/): A utility-first CSS framework for streamlined web application styling. 14 | - [EdgeStore](https://edgestore.dev/): A service for storing and managing data. 15 | 16 | ## Key Features 17 | 18 | - Real-time database 🔗 19 | - Notion-style editor 📝 20 | - Light and Dark mode 🌓 21 | - Infinite children documents 🌲 22 | - Trash can & soft delete 🗑️ 23 | - Authentication 🔐 24 | - File upload, deletion, and replacement 25 | - Icons for each document (changes in real-time) 🌠 26 | - Expandable and fully collapsable sidebar ➡️🔀⬅️ 27 | - Full mobile responsiveness 📱 28 | - Publish your note to the web 🌐 29 | - Landing page 🛬 30 | - Cover image for each document 🖼️ 31 | - Recover deleted files 🔄📄 32 | 33 | ## Setting Up the Project Locally 34 | 35 | ### Prerequisites 36 | 37 | Ensure that you have Node.js version 18.x.x or higher installed. 38 | 39 | ### Cloning the Repository 40 | 41 | ```shell 42 | git clone https://github.com/Nick-h4ck3r/quickflow.git 43 | cd quickflow 44 | ``` 45 | 46 | ### Installing Dependencies 47 | 48 | ```shell 49 | npm i 50 | ``` 51 | 52 | ### Setting Up Convex 53 | 54 | 1. Create an account on [convex.dev](https://www.convex.dev/) 55 | 2. Run the following command: 56 | 57 | ```shell 58 | npx convex dev 59 | ``` 60 | 61 | 3. Follow the prompts to log in and select `a new project` in terminal. 62 | 63 | ![convex create new project](image-3.png) 64 | 65 | 4. A `.env.local` file will be created with `CONVEX_DEPLOYMENT` and `NEXT_PUBLIC_CONVEX_URL` variables. 66 | 67 | Keep this terminal running, as the Convex dev server is running locally. If it crashes or closes, run `npx convex dev` again. 68 | 69 | ### Setting Up Clerk for Authentication 70 | 71 | 1. Create an account on [clerk.com](https://clerk.com/) 72 | 2. Create a new application in the dashboard. 73 | 3. Enable social sign-ins (Google and GitHub recommended) and disable email option. 74 | 4. Copy the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` to your `.env.local` file. 75 | 5. Now, click on JWT Templates (in left sidebar) option in convex project dashboard. 76 | 77 | ![JWT Templates (in left sidebar) option in convex project dashboard.](image-2.png) 78 | 79 | 6. Click on `Create New Template` and select `convex`. 80 | 81 | 7. Copy the issuer URL and update the `domain` in `convex/auth.config.js`: 82 | 83 | ```javascript 84 | export default { 85 | providers: [ 86 | { 87 | domain: "https://your-issuer-url.clerk.accounts.dev", 88 | applicationID: "convex", 89 | }, 90 | ], 91 | }; 92 | ``` 93 | 94 | ### Setting Up EdgeStore for Data Storage 95 | 96 | 1. Create an account on [edgestore.dev](https://edgestore.dev/) 97 | 2. Create a new project in the dashboard. 98 | 3. Copy the `EDGE_STORE_ACCESS_KEY` and `EDGE_STORE_SECRET_KEY` to your `.env.local` file. 99 | 100 | ### Finalizing Environment Setup 101 | 102 | Ensure your `.env.local` file looks like this: 103 | 104 | ``` 105 | CONVEX_DEPLOYMENT= 106 | NEXT_PUBLIC_CONVEX_URL= 107 | 108 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 109 | CLERK_SECRET_KEY= 110 | 111 | EDGE_STORE_ACCESS_KEY= 112 | EDGE_STORE_SECRET_KEY= 113 | ``` 114 | 115 | ### Starting the Development Server 116 | 117 | ```shell 118 | npm run dev 119 | ``` 120 | 121 | ## Deployment 122 | 123 | To deploy the app on Vercel: 124 | 125 | 1. Import the project to deploy on vecrel. 126 | 2. Replace the build command with: `npm run build && npx convex deploy` 127 | 3. To get production env for convex, go to convex project, change the environment to `production`. 128 | 4. Click on `settings`, and copy the `Deployment URL` to `NEXT_PUBLIC_CONVEX_URL`. 129 | 5.After that, click on `Generate Production Deploy Key`, and copy it too. 130 | 6. Add the following environment variables in vercel deployment. 131 | - `CONVEX_DEPLOY_KEY`: convex-generate-production-deploy-key-here 132 | - `CONVEX_DEPLOYMENT`: convex-generate-production-deploy-key-here 133 | - `NEXT_PUBLIC_CONVEX_URL` : convex-deployment-url-here 134 | - `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`: Same as in `.env.local` 135 | - `CLERK_SECRET_KEY`: Same as in `.env.local` 136 | - `EDGE_STORE_ACCESS_KEY`: Same as in `.env.local` 137 | - `EDGE_STORE_SECRET_KEY`: Same as in `.env.local` 138 | 139 | ![vercel environment variables](image-4.png) 140 | 141 | ## Contributing 142 | 143 | Contributions are welcome! Please feel free to submit a Pull Request. 144 | 145 | ## Credits 146 | 147 | Special thanks to Antonio for his invaluable tutorial, which served as the foundation for building this app. You can find the tutorial [here](https://youtu.be/0OaDyjB9Ib8?si=D38xIsi46hG7M2sC). 148 | 149 | ## License 150 | 151 | [MIT License](LICENSE) -------------------------------------------------------------------------------- /app/(main)/(routes)/documents/[documentId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | import { useMemo } from "react"; 5 | import { Cover } from "@/components/cover"; 6 | import { Toolbar } from "@/components/toolbar"; 7 | import { Skeleton } from "@/components/ui/skeleton"; 8 | import { api } from "@/convex/_generated/api"; 9 | import { Id } from "@/convex/_generated/dataModel"; 10 | import { useMutation, useQuery } from "convex/react"; 11 | 12 | interface DocumentIdPageProps { 13 | params: { 14 | documentId: Id<"documents">; 15 | }; 16 | } 17 | 18 | const DocumentIdPage = ({ params }: DocumentIdPageProps) => { 19 | const Editor = useMemo( 20 | () => dynamic(() => import("@/components/editor"), { ssr: false }), 21 | [], 22 | ); 23 | const document = useQuery(api.documents.getById, { 24 | documentId: params.documentId, 25 | }); 26 | 27 | const update = useMutation(api.documents.update); 28 | 29 | const onChange = (content: string) => { 30 | update({ 31 | id: params.documentId, 32 | content, 33 | }); 34 | }; 35 | 36 | if (document === undefined) { 37 | return ( 38 |
39 | 40 |
41 |
42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 | ); 50 | } 51 | 52 | if (document === null) { 53 | return
Not found
; 54 | } 55 | 56 | return ( 57 |
58 | 59 |
60 | 61 | 62 |
63 |
64 | ); 65 | }; 66 | 67 | export default DocumentIdPage; 68 | -------------------------------------------------------------------------------- /app/(main)/(routes)/documents/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // import hooks 4 | import { useMutation } from "convex/react"; 5 | import { useUser } from "@clerk/clerk-react"; 6 | 7 | // import components 8 | import { Button } from "@/components/ui/button"; 9 | import { PlusCircle } from "lucide-react"; 10 | import Image from "next/image"; 11 | import { toast } from "sonner"; 12 | 13 | import { api } from "@/convex/_generated/api"; 14 | import { useRouter } from "next/navigation"; 15 | 16 | function Page() { 17 | const { user } = useUser(); 18 | const router = useRouter(); 19 | 20 | const create = useMutation(api.documents.create); 21 | 22 | const onCreate = () => { 23 | const promise = create({ title: "Untitled" }).then((docId) => { 24 | router.push(`/documents/${docId}`); 25 | }); 26 | 27 | toast.promise(promise, { 28 | loading: "Creating a new note...", 29 | success: "New note created!", 30 | error: "Failed to create a note.", 31 | }); 32 | }; 33 | 34 | return ( 35 |
36 | empty 43 | empty 50 |

51 | Welcome to {user?.firstName}'s Quickflow 52 |

53 | 56 |
57 | ); 58 | } 59 | 60 | export default Page; 61 | -------------------------------------------------------------------------------- /app/(main)/_components/Banner.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from "convex/react"; 2 | import { useParams, useRouter } from "next/navigation"; 3 | 4 | import { toast } from "sonner"; 5 | 6 | import { api } from "@/convex/_generated/api"; 7 | import { Id } from "@/convex/_generated/dataModel"; 8 | import { Button } from "@/components/ui/button"; 9 | import { ConfirmModal } from "@/components/modals/confirm-modal"; 10 | 11 | interface BannerProps { 12 | documentId: Id<"documents">; 13 | } 14 | 15 | export const Banner = ({ documentId }: BannerProps) => { 16 | const router = useRouter(); 17 | 18 | const remove = useMutation(api.documents.remove); 19 | const restore = useMutation(api.documents.restore); 20 | 21 | const onRemove = () => { 22 | const promise = remove({ id: documentId }); 23 | 24 | toast.promise(promise, { 25 | loading: "Deleting note...", 26 | success: "Note deleted!", 27 | error: " Failed to delete note.", 28 | }); 29 | 30 | router.push("/documents"); 31 | }; 32 | 33 | const onRestore = () => { 34 | const promise = restore({ id: documentId }); 35 | 36 | toast.promise(promise, { 37 | loading: "Restoring note...", 38 | success: "Note restored!", 39 | error: " Failed to restore note.", 40 | }); 41 | }; 42 | 43 | return ( 44 |
45 |

This page has been archived.

46 | 54 | 55 | 62 | 63 |
64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /app/(main)/_components/DocumentList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useParams, useRouter } from "next/navigation"; 4 | import { useState } from "react"; 5 | 6 | import { Doc, Id } from "@/convex/_generated/dataModel"; 7 | import { useQuery } from "convex/react"; 8 | import { api } from "@/convex/_generated/api"; 9 | import { Item } from "./Item"; 10 | import { cn } from "@/lib/utils"; 11 | import { FileIcon } from "lucide-react"; 12 | 13 | interface DocumentListProps { 14 | parentDocumentId?: Id<"documents">; 15 | level?: number; 16 | data?: Doc<"documents">[]; 17 | } 18 | 19 | export const DocumentList = ({ 20 | parentDocumentId, 21 | level = 0, 22 | }: DocumentListProps) => { 23 | const params = useParams(); 24 | const router = useRouter(); 25 | const [expanded, setExpanded] = useState>({}); 26 | 27 | const onExpand = (documentId: string) => { 28 | setExpanded((prevExpanded) => ({ 29 | ...prevExpanded, 30 | [documentId]: !prevExpanded[documentId], 31 | })); 32 | }; 33 | 34 | const documents = useQuery(api.documents.getSidebar, { 35 | parentDocument: parentDocumentId, 36 | }); 37 | 38 | const onRedirect = (documentId: string) => { 39 | router.push(`/documents/${documentId}`); 40 | }; 41 | 42 | if (documents === undefined) { 43 | return ( 44 | <> 45 | 46 | {level === 0 && ( 47 | <> 48 | 49 | 50 | 51 | )} 52 | 53 | ); 54 | } 55 | 56 | return ( 57 | <> 58 |

66 | No pages inside 67 |

68 | 69 | {documents.map((document) => ( 70 |
71 | onRedirect(document._id)} 75 | label={document.title} 76 | icon={FileIcon} 77 | documentIcon={document.icon} 78 | active={params.documentId === document._id} 79 | level={level} 80 | onExpand={() => onExpand(document._id)} 81 | expanded={expanded[document._id]} 82 | /> 83 | {expanded[document._id] && ( 84 | 85 | )} 86 |
87 | ))} 88 | 89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /app/(main)/_components/Item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useUser } from "@clerk/clerk-react"; 4 | import { Skeleton } from "@/components/ui/skeleton"; 5 | import { api } from "@/convex/_generated/api"; 6 | import { Id } from "@/convex/_generated/dataModel"; 7 | import { cn } from "@/lib/utils"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuTrigger, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuSeparator, 14 | } from "@/components/ui/dropdown-menu"; 15 | import { useMutation, useQuery } from "convex/react"; 16 | import { GenericId } from "convex/values"; 17 | import { 18 | ChevronDown, 19 | ChevronRight, 20 | LucideIcon, 21 | MoreHorizontal, 22 | Plus, 23 | Trash, 24 | Copy, 25 | } from "lucide-react"; 26 | import { useRouter } from "next/navigation"; 27 | import { toast } from "sonner"; 28 | 29 | interface ItemProps { 30 | // optional props 31 | id?: Id<"documents">; 32 | documentIcon?: string; 33 | active?: boolean; 34 | expanded?: boolean; 35 | isSearch?: boolean; 36 | level?: number; 37 | onExpand?: () => void; 38 | 39 | // required props 40 | label: string; 41 | onClick?: () => void; 42 | icon: LucideIcon; 43 | } 44 | 45 | export const Item = ({ 46 | id, 47 | label, 48 | onClick, 49 | icon: Icon, 50 | active, 51 | documentIcon, 52 | level = 0, 53 | isSearch, 54 | onExpand, 55 | expanded, 56 | }: ItemProps) => { 57 | const { user } = useUser(); 58 | const router = useRouter(); 59 | const create = useMutation(api.documents.create); 60 | const archive = useMutation(api.documents.archive); 61 | const copy = useMutation(api.documents.copy); 62 | 63 | const document = useQuery(api.documents.getById, id ? { 64 | documentId: id as Id<"documents">, 65 | } : "skip"); 66 | 67 | const handleExpand = (e: React.MouseEvent) => { 68 | e.stopPropagation(); 69 | onExpand?.(); 70 | }; 71 | 72 | const onCreate = (e: React.MouseEvent) => { 73 | e.stopPropagation(); 74 | if (!id) return; 75 | 76 | const promise = create({ title: "Untitled", parentDocument: id }).then( 77 | (documentId) => { 78 | if (!expanded) { 79 | onExpand?.(); 80 | } 81 | router.push(`/documents/${documentId}`); 82 | }, 83 | ); 84 | toast.promise(promise, { 85 | loading: "Creating a new note...", 86 | success: "New note created!", 87 | error: "Failed to create a note.", 88 | }); 89 | }; 90 | 91 | const onArchive = (e: React.MouseEvent) => { 92 | e.stopPropagation(); 93 | 94 | if (!id) return; 95 | const promise = archive({ id }); 96 | 97 | toast.promise(promise, { 98 | loading: "Moving to trash...", 99 | success: "Note moved to trash!", 100 | error: "Failed to archive a note.", 101 | }); 102 | 103 | router.push("/documents"); 104 | }; 105 | 106 | const onCopy = (e: React.MouseEvent) => { 107 | e.stopPropagation(); 108 | if (!id || !document) return; 109 | 110 | const promise = copy({ 111 | title: `Copy of ${document?.title}`, 112 | content: document?.content, 113 | coverImage: document?.coverImage, 114 | icon: document?.icon, 115 | parentDocument: document?.parentDocument, 116 | }).then((documentId) => { 117 | if (!expanded) { 118 | onExpand?.(); 119 | } 120 | router.push(`/documents/${documentId}`); 121 | }); 122 | toast.promise(promise, { 123 | loading: "Copying a new note...", 124 | success: "Note copied!", 125 | error: "Failed to copy the note.", 126 | }); 127 | }; 128 | 129 | const ChevronIcon = expanded ? ChevronDown : ChevronRight; 130 | 131 | return ( 132 |
141 | {!!id && ( 142 |
147 | 148 |
149 | )} 150 | 151 | {documentIcon ? ( 152 |
{documentIcon}
153 | ) : ( 154 | 155 | )} 156 | 157 | {label} 158 | 159 | {isSearch && ( 160 | 161 | ctrl + q 162 | 163 | )} 164 | 165 | {!!id && ( 166 |
167 | 168 | e.stopPropagation()} asChild> 169 |
173 | 174 |
175 |
176 | 182 | 183 | 184 | Delete 185 | 186 | 190 | 191 | Make a copy 192 | 193 | 194 |
195 | Last edited by: {user?.fullName} 196 |
197 |
198 |
199 |
204 | 205 |
206 |
207 | )} 208 |
209 | ); 210 | }; 211 | 212 | Item.Skeleton = function ItemSkeleton({ level }: { level?: number }) { 213 | return ( 214 |
220 | 221 | 222 |
223 | ); 224 | }; 225 | -------------------------------------------------------------------------------- /app/(main)/_components/Menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Id } from "@/convex/_generated/dataModel"; 4 | import { api } from "@/convex/_generated/api"; 5 | 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuTrigger, 10 | DropdownMenuItem, 11 | DropdownMenuSeparator, 12 | } from "@/components/ui/dropdown-menu"; 13 | import { MenuIcon, MoreHorizontalIcon, TrashIcon } from "lucide-react"; 14 | import { Button } from "@/components/ui/button"; 15 | 16 | import { toast } from "sonner"; 17 | 18 | import { useMutation } from "convex/react"; 19 | import { useRouter } from "next/navigation"; 20 | import { useUser } from "@clerk/clerk-react"; 21 | import { Skeleton } from "@/components/ui/skeleton"; 22 | 23 | interface MenuProps { 24 | documentId: Id<"documents">; 25 | } 26 | 27 | export const Menu = ({ documentId }: MenuProps) => { 28 | const router = useRouter(); 29 | const { user } = useUser(); 30 | const archive = useMutation(api.documents.archive); 31 | 32 | const onArchive = () => { 33 | const promise = archive({ id: documentId }); 34 | 35 | toast.promise(promise, { 36 | loading: "Moving to trash...", 37 | success: "Note moved to trash!", 38 | error: "Failed to archive a note.", 39 | }); 40 | 41 | router.push("/documents"); 42 | }; 43 | 44 | return ( 45 | 46 | 47 | 50 | 51 | 57 | 58 | 59 | Delete 60 | 61 | 62 |
63 | Last edited by: {user?.fullName} 64 |
65 |
66 |
67 | ); 68 | }; 69 | 70 | Menu.Skeleton = function MenuSkeleton() { 71 | return ; 72 | }; 73 | -------------------------------------------------------------------------------- /app/(main)/_components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMutation, useQuery } from "convex/react"; 4 | import { useParams, useRouter } from "next/navigation"; 5 | 6 | import { api } from "@/convex/_generated/api"; 7 | import { Id } from "@/convex/_generated/dataModel"; 8 | 9 | import { Title } from "./Title"; 10 | import { Banner } from "./Banner"; 11 | import { Menu } from "./Menu"; 12 | 13 | import { Button } from "@/components/ui/button"; 14 | import { Copy, MenuIcon } from "lucide-react"; 15 | import { Publish } from "./Publish"; 16 | import { toast } from "sonner"; 17 | 18 | interface NavbarProps { 19 | isCollapsed: boolean; 20 | onResetWidth: () => void; 21 | } 22 | 23 | function Navbar({ isCollapsed, onResetWidth }: NavbarProps) { 24 | const params = useParams(); 25 | const router = useRouter(); 26 | const copy = useMutation(api.documents.copy); 27 | const document = useQuery(api.documents.getById, { 28 | documentId: params.documentId as Id<"documents">, 29 | }); 30 | 31 | if (document === undefined) { 32 | return ( 33 |
34 | 35 |
36 | 37 |
38 |
39 | ); 40 | } 41 | 42 | if (document === null) { 43 | return null; 44 | } 45 | 46 | const onCopy = (e: React.MouseEvent) => { 47 | e.stopPropagation(); 48 | 49 | const promise = copy({ 50 | title: `Copy of ${document?.title}`, 51 | content: document?.content, 52 | coverImage: document?.coverImage, 53 | icon: document?.icon, 54 | parentDocument: document?.parentDocument, 55 | }).then((documentId) => { 56 | router.push(`/documents/${documentId}`); 57 | }); 58 | toast.promise(promise, { 59 | loading: "Copying a new note...", 60 | success: "Note copied!", 61 | error: "Failed to copy the note.", 62 | }); 63 | }; 64 | return ( 65 | <> 66 |