├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── app ├── (landing) │ ├── _components │ │ ├── Footer.tsx │ │ ├── Heading.tsx │ │ ├── Heroes.tsx │ │ ├── Logo.tsx │ │ └── Navbar.tsx │ ├── layout.tsx │ └── page.tsx ├── (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 ├── (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 │ ├── ConfirmModal.tsx │ ├── CoverImageModal.tsx │ └── SettingsModal.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 ├── useCoverImage.tsx ├── useOrigin.tsx ├── useScrollTop.tsx ├── useSearch.tsx └── useSettings.tsx ├── lib ├── edgestore.ts └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── empty-dark.svg ├── empty.svg ├── error-dark.svg ├── error.svg ├── idea-dark.svg ├── idea.svg ├── logo-dark.svg ├── logo.svg ├── team-dark.svg └── team.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 | .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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zotion 2 | 3 | This project is a simplified clone of the popular productivity application, Notion. It's designed to replicate some of the core features of Notion, providing a platform where users can create, edit, and organize their notes in a flexible and intuitive interface. 4 | 5 | It uses Convex as the backend, which is a real-time database that allows for instant data updates. The application also uses Edgestore, a distributed key-value store, to manage the images and files uploaded by the users.The user authentication is handled by Clerk, a secure and scalable user authentication API. 6 | 7 | ## Live 8 | 9 | Zotion - [https://zotion-app.vercel.app/](https://zotion-app.vercel.app/) 10 | 11 | ## Features 12 | 13 | **Productivity and Organization**s 14 | 15 | - 📝 Notion-style editor for seamless note-taking 16 | - 📂 Infinite children documents for hierarchical organization 17 | - ➡️🔀⬅️ Expandable and fully collapsible sidebar for easy navigation 18 | - 🎨 Customizable icons for each document, updating in real-time 19 | - 🗑️ Trash can with soft delete and file recovery options 20 | 21 | **User Experience** 22 | 23 | - 🌓 Light and Dark mode to suit preferences 24 | - 📱 Full mobile responsiveness for productivity on the go 25 | - 🛬 Landing page for a welcoming user entry point 26 | - 🖼️ Cover image for each document to add a personal touch 27 | 28 | **Data Management** 29 | 30 | - 🔄 Real-time database for instant data updates 31 | - 📤📥 File upload, deletion, and replacement options 32 | 33 | **Security and Sharing** 34 | 35 | - 🔐 Authentication to secure notes 36 | - 🌍 Option to publish your note to the web for sharing 37 | 38 | ## Technologies 39 | 40 | ![NextJS](https://img.shields.io/badge/Next-black?style=for-the-badge&logo=next.js&logoColor=white) 41 | ![Shadcn-ui](https://img.shields.io/badge/shadcn/ui-000000.svg?style=for-the-badge&logo=shadcn/ui&logoColor=white) 42 | ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6.svg?style=for-the-badge&logo=TypeScript&logoColor=white) 43 | ![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-38B2AC.svg?style=for-the-badge&logo=Tailwind-CSS&logoColor=white) 44 | ![Clerk](https://img.shields.io/badge/Clerk-6C47FF.svg?style=for-the-badge&logo=Clerk&logoColor=white) 45 | ![Convex](https://img.shields.io/badge/Convex-ee342f.svg?style=for-the-badge&logo=Convex&logoColor=white) 46 | ![Edgestore](https://img.shields.io/badge/Edgestore-a57fff.svg?style=for-the-badge&logo=Edgestore&logoColor=white) 47 | ![Blocknote](https://img.shields.io/badge/Blocknote-ff8c00.svg?style=for-the-badge&logo=Blocknote&logoColor=white) 48 | 49 | ## Installation 50 | 51 | 1. Clone the repository 52 | 2. Install the dependencies 53 | 54 | ``` 55 | npm install 56 | ``` 57 | 58 | 3. Set up the environment variables 59 | 60 | ``` 61 | # Deployment used by `npx convex dev` 62 | CONVEX_DEPLOYMENT= 63 | NEXT_PUBLIC_CONVEX_URL= 64 | 65 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 66 | CLERK_SECRET_KEY= 67 | 68 | EDGE_STORE_ACCESS_KEY= 69 | EDGE_STORE_SECRET_KEY= 70 | ``` 71 | 72 | 4. Run Convex 73 | 74 | ``` 75 | npx convex dev 76 | ``` 77 | 78 | 5. Run the development server 79 | 80 | ``` 81 | npm run dev 82 | ``` 83 | 84 | ## Acknowledgements 85 | 86 | [CodewithAntonio](https://www.youtube.com/@codewithantonio) 87 | -------------------------------------------------------------------------------- /app/(landing)/_components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Logo } from "./Logo"; 3 | 4 | export const Footer = () => { 5 | return ( 6 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /app/(landing)/_components/Heading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Spinner } from "@/components/spinner"; 4 | import { Button } from "@/components/ui/button"; 5 | import { SignInButton } from "@clerk/clerk-react"; 6 | import { useConvexAuth } from "convex/react"; 7 | import { ArrowRight } from "lucide-react"; 8 | import Link from "next/link"; 9 | 10 | export const Heading = () => { 11 | const { isAuthenticated, isLoading } = useConvexAuth(); 12 | 13 | return ( 14 |
15 |

16 | Your Ideas💡, Documents📕, & Plans🚀. Welcome to{" "} 17 | Zotion 18 |

19 |

20 | Zotion is the connected workspace where
better, faster work 21 | happens. 22 |

23 | {isLoading && ( 24 |
25 | 26 |
27 | )} 28 | {isAuthenticated && !isLoading && ( 29 | 35 | )} 36 | {!isAuthenticated && !isLoading && ( 37 | 38 | 42 | 43 | )} 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /app/(landing)/_components/Heroes.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export const Heroes = () => { 4 | return ( 5 |
6 |
7 |
8 | Idea 14 | Idea 20 |
21 |
22 | Team 28 | Team 34 |
35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /app/(landing)/_components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { Poppins } from "next/font/google"; 2 | import { cn } from "@/lib/utils"; 3 | import Image from "next/image"; 4 | 5 | const font = Poppins({ 6 | subsets: ["latin"], 7 | weight: ["400", "600"], 8 | }); 9 | 10 | export const Logo = () => { 11 | return ( 12 |
13 | logo 20 | logo 27 |

Zotion

28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /app/(landing)/_components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useScrollTop } from "@/hooks/useScrollTop"; 4 | import { cn } from "@/lib/utils"; 5 | import { Logo } from "./Logo"; 6 | import { ModeToggle } from "@/components/mode-toggle"; 7 | import { useConvexAuth } from "convex/react"; 8 | import { SignInButton, UserButton } from "@clerk/clerk-react"; 9 | import { Button } from "@/components/ui/button"; 10 | import { Spinner } from "@/components/spinner"; 11 | import Link from "next/link"; 12 | 13 | export const Navbar = () => { 14 | const { isAuthenticated, isLoading } = useConvexAuth(); 15 | const scrolled = useScrollTop(); 16 | 17 | return ( 18 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /app/(landing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "./_components/Navbar"; 2 | 3 | const LandingLayout = ({ children }: { children: React.ReactNode }) => { 4 | return ( 5 |
6 | 7 |
{children}
8 |
9 | ); 10 | }; 11 | export default LandingLayout; 12 | -------------------------------------------------------------------------------- /app/(landing)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from "./_components/Footer"; 2 | import { Heading } from "./_components/Heading"; 3 | import { Heroes } from "./_components/Heroes"; 4 | 5 | export default function LandingPage() { 6 | return ( 7 |
8 |
9 | 10 | 11 |
12 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /app/(main)/(routes)/documents/[documentId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | import { useMemo } from "react"; 5 | 6 | import { Cover } from "@/components/cover"; 7 | import { Toolbar } from "@/components/toolbar"; 8 | import { Skeleton } from "@/components/ui/skeleton"; 9 | 10 | import { api } from "@/convex/_generated/api"; 11 | import { Id } from "@/convex/_generated/dataModel"; 12 | import { useMutation, useQuery } from "convex/react"; 13 | 14 | interface DocumentIdPageProps { 15 | params: { 16 | documentId: Id<"documents">; 17 | }; 18 | } 19 | 20 | const DocumentIdPage = ({ params }: DocumentIdPageProps) => { 21 | const Editor = useMemo( 22 | () => dynamic(() => import("@/components/editor"), { ssr: false }), 23 | [], 24 | ); 25 | 26 | const document = useQuery(api.documents.getById, { 27 | documentId: params.documentId, 28 | }); 29 | 30 | const update = useMutation(api.documents.update); 31 | 32 | const onChange = (content: string) => { 33 | update({ 34 | id: params.documentId, 35 | content, 36 | }); 37 | }; 38 | 39 | if (document === undefined) { 40 | return ( 41 |
42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 |
50 |
51 |
52 | ); 53 | } 54 | 55 | if (document === null) { 56 | return
Not found
; 57 | } 58 | 59 | return ( 60 |
61 | 62 |
63 | 64 | 65 |
66 |
67 | ); 68 | }; 69 | export default DocumentIdPage; 70 | -------------------------------------------------------------------------------- /app/(main)/(routes)/documents/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { useUser } from "@clerk/clerk-react"; 5 | import { Button } from "@/components/ui/button"; 6 | import { PlusCircle } from "lucide-react"; 7 | import { useMutation } from "convex/react"; 8 | import { api } from "@/convex/_generated/api"; 9 | import { toast } from "sonner"; 10 | import { useRouter } from "next/navigation"; 11 | 12 | const DocumentsPage = () => { 13 | const { user } = useUser(); 14 | const router = useRouter(); 15 | const create = useMutation(api.documents.create); 16 | 17 | const onCreate = () => { 18 | const promise = create({ title: "Untitled" }).then((documentId) => 19 | router.push(`/documents/${documentId}`), 20 | ); 21 | 22 | toast.promise(promise, { 23 | loading: "Creating a new note....", 24 | success: "New note created!", 25 | error: "Failed to create a new note.", 26 | }); 27 | }; 28 | 29 | return ( 30 |
31 | empty 39 | empty 47 |

48 | Welcome to {user?.firstName}'s Zotion 49 |

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

45 | This page is in the Trash. 46 |

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

68 | No pages inside 69 |

70 | {documents?.map((document) => ( 71 |
72 | 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 { Skeleton } from "@/components/ui/skeleton"; 4 | import { api } from "@/convex/_generated/api"; 5 | import { Id } from "@/convex/_generated/dataModel"; 6 | import { cn } from "@/lib/utils"; 7 | import { useRouter } from "next/navigation"; 8 | import { useMutation } from "convex/react"; 9 | import { toast } from "sonner"; 10 | import { 11 | DropdownMenu, 12 | DropdownMenuSeparator, 13 | DropdownMenuTrigger, 14 | DropdownMenuContent, 15 | DropdownMenuItem, 16 | } from "@/components/ui/dropdown-menu"; 17 | import { 18 | ChevronDown, 19 | ChevronRight, 20 | LucideIcon, 21 | MoreHorizontal, 22 | Plus, 23 | Trash, 24 | } from "lucide-react"; 25 | import { useUser } from "@clerk/clerk-react"; 26 | 27 | interface ItemProps { 28 | id?: Id<"documents">; 29 | documentIcon?: string; 30 | active?: boolean; 31 | expanded?: boolean; 32 | isSearch?: boolean; 33 | level?: number; 34 | onExpand?: () => void; 35 | label: string; 36 | onClick?: () => void; 37 | icon: LucideIcon; 38 | } 39 | 40 | export const Item = ({ 41 | id, 42 | label, 43 | onClick, 44 | icon: Icon, 45 | active, 46 | documentIcon, 47 | isSearch, 48 | level = 0, 49 | onExpand, 50 | expanded, 51 | }: ItemProps) => { 52 | const { user } = useUser(); 53 | const router = useRouter(); 54 | const create = useMutation(api.documents.create); 55 | const archive = useMutation(api.documents.archive); 56 | 57 | const onArchive = (event: React.MouseEvent) => { 58 | event.stopPropagation(); 59 | if (!id) return; 60 | const promise = archive({ id }).then(() => router.push("/documents")); 61 | 62 | toast.promise(promise, { 63 | loading: "Moving to trash...", 64 | success: "Note moved to trash!", 65 | error: "Failed to archive note.", 66 | }); 67 | }; 68 | 69 | const handleExpand = ( 70 | event: React.MouseEvent, 71 | ) => { 72 | event.stopPropagation(); 73 | onExpand?.(); 74 | }; 75 | 76 | const onCreate = (event: React.MouseEvent) => { 77 | event.stopPropagation(); 78 | if (!id) return; 79 | 80 | const promise = create({ title: "Untitled", parentDocument: id }).then( 81 | (documentId) => { 82 | if (!expanded) { 83 | onExpand?.(); 84 | } 85 | router.push(`/documents/${documentId}`); 86 | }, 87 | ); 88 | 89 | toast.promise(promise, { 90 | loading: "Creating new note", 91 | success: "New note created.", 92 | error: "Failed to create note.", 93 | }); 94 | }; 95 | 96 | const ChevronIcon = expanded ? ChevronDown : ChevronRight; 97 | 98 | return ( 99 |
108 | {!!id && ( 109 |
114 | 115 |
116 | )} 117 | {documentIcon ? ( 118 |
{documentIcon}
119 | ) : ( 120 | 121 | )} 122 | 123 | {label} 124 | {isSearch && ( 125 | 126 | CTRLK 127 | 128 | )} 129 | {!!id && ( 130 |
131 | 132 | e.stopPropagation()} asChild> 133 |
137 | 138 |
139 |
140 | 146 | 147 | 148 | Delete 149 | 150 | 151 |
152 | Last edited by: {user?.fullName} 153 |
154 |
155 |
156 |
161 | 162 |
163 |
164 | )} 165 |
166 | ); 167 | }; 168 | 169 | Item.Skeleton = function ItemSkeleton({ level }: { level?: number }) { 170 | return ( 171 |
175 | 176 | 177 |
178 | ); 179 | }; 180 | -------------------------------------------------------------------------------- /app/(main)/_components/Menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { Id } from "@/convex/_generated/dataModel"; 5 | import { useUser } from "@clerk/clerk-react"; 6 | import { useMutation } from "convex/react"; 7 | import { api } from "@/convex/_generated/api"; 8 | import { toast } from "sonner"; 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuSeparator, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu"; 16 | import { MoreHorizontal, Trash } from "lucide-react"; 17 | import { Button } from "@/components/ui/button"; 18 | import { Skeleton } from "@/components/ui/skeleton"; 19 | 20 | interface MenuProps { 21 | documentId: Id<"documents">; 22 | } 23 | 24 | export const Menu = ({ documentId }: MenuProps) => { 25 | const router = useRouter(); 26 | const { user } = useUser(); 27 | 28 | const archive = useMutation(api.documents.archive); 29 | 30 | const onArchive = () => { 31 | const promise = archive({ id: documentId }); 32 | 33 | toast.promise(promise, { 34 | loading: "Moving to trash...", 35 | success: "Note moved to trash!", 36 | error: "Failed to archive note.", 37 | }); 38 | 39 | router.push("/documents"); 40 | }; 41 | 42 | return ( 43 | 44 | 45 | 48 | 49 | 55 | 56 | 57 | Delete 58 | 59 | 60 |
61 | Last edited by {user?.fullName} 62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | Menu.Skeleton = function MenuSkeleton() { 69 | return ; 70 | }; 71 | -------------------------------------------------------------------------------- /app/(main)/_components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { api } from "@/convex/_generated/api"; 4 | import { Id } from "@/convex/_generated/dataModel"; 5 | import { useQuery } from "convex/react"; 6 | import { MenuIcon } from "lucide-react"; 7 | import { useParams } from "next/navigation"; 8 | import { Title } from "./Title"; 9 | import { Banner } from "./Banner"; 10 | import { Menu } from "./Menu"; 11 | import { Publish } from "./Publish"; 12 | 13 | interface NavbarProps { 14 | isCollapsed: boolean; 15 | onResetWidth: () => void; 16 | } 17 | 18 | export const Navbar = ({ isCollapsed, onResetWidth }: NavbarProps) => { 19 | const params = useParams(); 20 | const document = useQuery(api.documents.getById, { 21 | documentId: params.documentId as Id<"documents">, 22 | }); 23 | 24 | if (document === undefined) { 25 | return ( 26 | 32 | ); 33 | } 34 | 35 | if (document === null) { 36 | return null; 37 | } 38 | 39 | return ( 40 | <> 41 |