{
  "video_id": "0OaDyjB9Ib8",
  "title": "Fullstack Notion Clone: Next.js 13,  React, Convex, Tailwind | Full Course 2023",
  "url": "https://www.youtube.com/watch?v=0OaDyjB9Ib8",
  "transcript": "Hey there, my name is Antonio and welcome to the newest video on my channel. In this tutorial, you're going to learn how to build a beautiful Notion clone, which we are going to call Jocean. As you can see, we're going to have a beautiful landing page with a navbar, with a footer and some nice images right here. We're also going to have a toggle of light and dark mode, which is going to reflect the entire application as well. So without further ado, let's see exactly what this application will be capable of. So I'm going to go ahead and click on Get Joshin Free right here. And I'm going to sign it using my GitHub. Once I authorize the sign in, I'm going to get redirected back to the landing page. And you can see now it says Enter Joshin. Once I'm inside of here, we have a nice message. Welcome to Antonio Joshin. And we have a big button to create our first note. So let's go ahead and do exactly that. As you can see, it's created in real time. It's added to the sidebar right here. And guess what? We can rename it in real time. As you can see, it's been renamed right here in the toolbar above, as well as in the file explorer on the left. And I can also do it from here, just like that. But now it's looking a little bland. So let me rename this to my first note and let me add a little icon to it. For example, let's use this rocket right here. As you can see, this has immediately been reflected here in the navbar and right here in the file explorer. I can also add a cover image using Edge Store. So let me add a beautiful image here from Unsplash and once it's loaded, you can see how it takes a nice little space here. From here, I can change the cover or I can completely remove it. Besides the icon and the cover image, we're going to have a Notion style editor, which means we can write commands with a slash. I can select the heading and I can write subtitle like that. I can also go ahead and select a smaller heading and write a description. Or I can just write regular text. Besides text, I can also embed an image in the editor itself. I can either use the embed by URL option or I can upload any image I want. So let me pick another image from Unsplash right here. And as you can see, I can also resize it so it fits exactly what I need. Besides text and images, I can also answer bullet list like this. I can also select a specific item. I can make it bold, italic. I can underline it or I can strike through it. I can also give it a background color like this. Besides bullet lists, we also have numbered lists. One, two, and three like that. Now let's explore what else can we do with this application. As you can see in the sidebar right here, I have a little plus icon, which means that I can add a children inside of this note. And I can as well rename it in real time. Perfect. And I can go ahead and delete it as well. And we can do that as many times as we want. Now you might be thinking, well, with all of this infinite children, at some point, we're going to run out of space here, right? Well, don't worry. just like in RealNotion, we can expand the sidebar and make it fit our content so we have room in our file explorer. Now you might be wondering what these three buttons do. Well, I can select and delete the entire note and all of its children. And if I take a look at my trash right here, you can see that all of that is here. I can now go ahead and filter by my first note and you can see that I can go ahead and click on that note. This is the first note that we created and now it has a banner saying this page is in trash. I have an option to restore the entire page or to delete it forever. So first, let's try restoring the page. And as you can see, this is a smart restore, meaning that it also restored everything else in the trash, which was children of this note. Beautiful. But what happens if I delete partially like this? Well, as you can see, only that part has been deleted. And if I go ahead and randomly revert these items back, you can see that they become standalone items because they no longer have a reverted parent. So we have a very smart implementation here. Besides this, we can also collapse the entire sidebar like that. We can also publish the note. So I'm going to pick this first note which we created and I'm going to click publish right here. So right now our work is private and only we can see it. But once we publish the note, it's publicly visible on the web. So I'm going to copy this link right here and I'm going to go ahead and paste it in incognito mode right here. And as you can see, I can now preview this note as a guest, but I cannot edit the icon, the cover image, the subtitle, nothing. I can just look at this. And let's see what happens once I click on publish. As you can see now, it says an error, something went wrong, and we can go back to the landing page as a guest. So we're going to have beautiful error messages, we're going to have loading skeletons, and much more. And just as I've shown you on the landing page, we can click on settings right here and we can change this to dark mode as well. And it fits beautifully in this application. But just right now, I'm going to change it back to light mode. I can also search through all of my documents. I can find my first note and I can click on it. I can also use a shortcut command key like this, and I can then go ahead and find another note and enter it just like this. So that's going to be our project. I think this is a very exciting project. We have a lot of new technologies to learn and a lot to do, but we're not done here. This entire application is also going to be fully responsive. As you can see right here, I'm on a mobile mode here and I have a collapsible nav bar. And from here, I can switch to my first note like that. I can open pop-ups. I can do everything I want from here. I can visit the trash, I can revert notes back, and I can visit them again. Beautiful. So I hope you're excited for this project because I sure am. And without further ado, let's get started. So let's get started by configuring our next 13 project. So here on the left side, I have my Visual Studio Code and I've prepared my terminal. You can use any terminal you want. If you want to access the one in Visual Studio Code like I have, I pressed on this little problems tab right here. And then I pressed on the terminal option like that. So the command I'm going to run is npx create-next-app at latest, meaning that we're going to use whatever is the latest version of the Create Next app. And then we're going to give our project a name. In my case, that's going to be a Notion-clone. And you can just go ahead and press enter. It's asking us whether we want to use TypeScript. For this tutorial, I'm going to use TypeScript. So you can select yes. for the S-Lint option you can select yes as well for Tailwind CSS select yes as well source directory leave it at no or select no yourself because we're not going to use the source directory in this tutorial for the app router this is very important make sure you select yes because we're going to be working with the new app router here and we're not going to be working with the pages folder as in the previous versions of next 13 so make sure you select yes for this option. It's asking us whether we want to customize the default import alias. So the default import alias is going to be an alias inside of our imports so that we don't have to write all of those long relative imports if we are in a deeply nested folder. You're going to see that in action once we start the tutorial but what I recommend you choose for this option is no. So leave it as it is. The default one is the add sign and we're going to be using that in this tutorial. So just select no for this option. And there we go. You can sit back, relax and wait for this to install. Great. So now that we have our next project configured, what I want to do is I want you to open this folder where it says that it created the Notion clone project. So I'm going to go ahead and find the open button right here. And I'm going to select Notion clone because that's the name of my project. If you get this prompt, feel free to just say, yes, I trust the authors like this. Great. So just make sure you're inside of your new Notion clone. To confirm that you installed it correctly, you should have the app folder with the favicon, with the globals layout and page.esx. You should also have the public folder, the node module folders, some Slin configuration, and a Tailwind and TypeScript configuration because we chose those options initially when we created the project. What I want to do now is initialize a ShadCN UI inside of the project. That is a library which is built on top of a Radix UI and we're gonna use it to style our entire project and it works very well with Tailwind. So it's perfect for our use case. So let's head back inside of our terminal right here. and I'm just getting a little update here. So let me pause the screen and update my shell. You don't have to do this, of course. All right, sorry for that. And now you can just go ahead and write npx chatcn-ui at latest init like this, which is short for initialize. And go ahead and press enter. And now it's asking us a couple of questions. So let's look at the first question. It's asking us whether we are using TypeScript. Remember, we initialize our entire project with TypeScript. That means we also want TypeScript components. So select yes for this one. For the style, go ahead and select default. For the color, it doesn't really matter. But if you wanted to look the closest to my tutorial, go ahead and choose neutral. You can of course play around and select your own dark color. But neutral in my case kind of gives me the best looking app similar to Notion. So select neutral for this option. Now it's asking where is your global.css file? Well, just a moment ago, we opened the app folder and we confirmed that it is there. So you can just press enter. Now it's asking us if we want to use CSS variables for colors and you can freely select yes for that option. Now, this is a tricky part. So in my case, it's asking me where is your tailwind.config.js located? But let's take a look in my browser right here. So don't close the terminal. I'm taking a look at my browser here and notice that I don have tailwind Instead I have So if you have the same thing if you have a file called tailwind instead of then you need to modify this command So all that is important is that it doesn't create two tailwind files for you. Don't worry if you mess this up. You can still work and your project will work and your styles will work and your tailwind will work even with two different config files, but let's do this properly. So go in your file browser and confirm whether you have a .TypeScript or a .JavaScript version of the Tailwind config. Then go back inside and what you can do here is you can press the tab key and then you can manually add it from .JS to .TS or if you actually have a .JS file, you can just leave it at .JS. In my case, I need a TS file, So I'm going to change it to .ts and press enter. Remember, I pressed the tab button to edit this placeholder, which was hard-coded in this interface. Go ahead and press enter. Now it's asking us to configure the import alias for components. Remember, when we configured our project, we set the default import alias to be an add sign, meaning that this placeholder is going to work. So we can safely press enter here. Same is true for our lib and utils. and it's asking us whether we are using server components. That is referring to the question in an X13 initialization where we selected the app router. Since we selected yes there, that means that we're also selecting yes here. And go ahead and confirm this configuration by pressing enter or the Y letter. And now just sit back, relax and wait for this to finish. And now that you finally set up the Shatsian UI, you can go ahead and run npm run dev inside of this repository. And that is going to start the Next.js project on localhost 3000. So I'm going to go ahead in my browser now, and I'm going to refresh this screen. And you should be seeing something similar to this. Great. So we're now going to modify this so it's actually more suitable for the project that we need. So let's go ahead inside of our app folder and let's see what we have here. So first we have the layout.tsx file. So layout files are kind of like group files which reflect the entire project. They can be global like this one which is reflecting every single route and every single component inside of our Next.js project or they can also be grouped inside a specific folder but we're gonna come to that a bit later. So layout is not the file which you would technically use to render something. Of course you can, but it's more of a file where you would put, for example, a reusable layout like a sidebar and a navigation bar, right? Whereas the page.tsx would be the one where you want your user to do something like a form or something like that. So first thing I wanna do is go inside of page.tsx file right here and I wanna remove everything inside of this return function. So let's go ahead and remove everything here. And let's go ahead and write a paragraph saying hello, Notion clone and save this. And now you should get all of that removed. And instead, you're going to have a nice little text, probably much smaller for you. So I just zoomed in a lot saying hello, Notion clone. And now what I'm going to do is I want to try out our CSS, right? So one thing that I want to show you is that I'm using an extension called Tailwind CSS. So I'm going to write Tailwind here. And there it is. It's the first option right here. And let me just close this. So it's the first option here. And it's called Tailwind CSS IntelliSense. Make sure you install this package because writing Tailwind is going to be much, much easier with that. For example, you can now open a class name here. and you can see that when I write font, for example, well, not always, but it's very helpful when it comes to colors, for example. Text dash red dash 500, like this. And you can see that now I have a little box here showing what color the text should be, all thanks to that extension. I can also do a font dash and there we go. Now, I told you sometimes it works, sometimes it doesn't. You don't even have to use this out of the complete, but sometimes it's very helpful when you want to confirm that the class you're writing actually exists. So let's go ahead and let's select font bold like this and save. And there we go. You can see how in my screen on the right here, I have hello Notion clone in bold red color. And with this extension, you can also hover over to confirm the CSS is actually what you think it is inside of that class name. Great, excellent. One more thing I wanna show you is how to install a ShadCN UI package. So for example, let's say we want a button component from the chat CN UI package. All you have to do is go back inside of your terminal. And for now, I'm just going to shut down this localhost 3000. And I'm going to write npx chat CN, chat CN dash UI at latest. And I'm going to go ahead and just expand this even a bit more. And I'm going to write add button like this. And go ahead and just press enter. and that is going to install the button component inside of your project. So you can now npm run dev your project again. And let me just zoom back in. And now if you take a look at your components folder, you're gonna notice that you have a new folder called UI and inside of it a button.tsx. And in here you have a production ready accessibility compatible button component all thanks to Shadzian UI. And the best part of it all, it is fully customizable. So you have a lot of useful variants here, like a destructive variant of it, outline variant of it, just like in any other component library which you would use. But what Shadzian offers us is to go directly inside of the source code and change it to our liking. So let's try that out. Go back inside of page.tsx where you wrote the Hello Notion clone. And for now, you can remove this paragraph here and instead open a div. and now you can go ahead and add the button from add slash components UI button like this and you can remove this image import. We no longer need it. So just add a button from add slash components UI button. Of course, make sure that you confirm that the button exists and that you run that chat scene command which we did just a moment ago. And now you can probably notice that my local host did not hot reload. Why is that happening? Well, a little tip for you. Every time you shut down your terminal like I did in order to install a button with this command and you then run it again, make sure you refresh your localhost because the hot reload was broken for a second. Great. And as you can see, we have a little button here. So what I'm going to do now is I'm not going to do a self closing tag. Instead, I'm going to write click me like this. And below that, I'm going to end the button. And there we go. Now we have a nice button which says click me. And if you saw the source code and we just did a moment ago together, you know that you can give it a variant prop and you can, for example, mark it as destructive, right? This would be something like trash or delete, something like that, a destructive icon, right? Or you can also make it outline like this. Whoops. And you can see that when you write something that doesn't exist, you get a TypeScript error. So let's actually fix it and write outline. There we go. Now you can see a brief little border around it. Or it can be secondary. like that. And now you can see it's not as noticeable. Perfect. So we have a very nice button, which we can easily customize to fit our Notion clone. Besides this, we can also give it a size, like a default icon, large, small, stuff like that. And the best part of it all is that we can always directly go inside of button like this. So either the way I did this is I hold the control or the command key, and then I press on the component that I want to visit. And then I have a little pop-up here showing me the definitions of that. And inside of here, there is a button.tsx file, and I can click here, and that's going to open that file directly. Or if you want to do it easier, you can just visit the components folder UI and find the button. So remember, we can always visit this component file ourselves, and we can change whatever we want. For example, you don't have to do this. I just want to show you how easy it is to add a new variant. Let's say we want a variant that our button is purple for whatever reason. So we can add that purple. I'm going to text-white bg-indigo-500 like that. And then I can go back here and guess what? I can now give my button a variant of purple like this. And look at it. It's purple now. And I can reuse this as many times as I want. But one cool thing that I can also do, if it's not something I want to reuse, if it is just a one-time thing, I can always pass a class name with bg-emerald, for example, 500 and text dash white. So you can see now I did not pass a variant because this is only going to be one time, right? We're not going to reuse this. Perfect. So that's why I like Shatzian so much, because it fits so well with Tailwind and because it allows us to customize it all the way to how rounded the borders are directly in the source code. So if you added this purple the same way I did, you can now safely remove it because we are not going to need it. And you can just leave the component as it is. Perfect. And we're going to leave it like this for now. In the next part, we're going to go ahead and further explore the Next.js structure. Excellent job. So now that we've set up our project, I want to show you a cool little extension that is able to go through your entire project and all the files and decide what linters you need to ensure the best code quality and standards in your project. So let's take a look right now. In this component, if I add a random space here, nothing is happening. Well, this is technically called a trailing space. and if I had a linter like git diff check or prettier or something like that I'm most likely to get a little underlined red error here saying that I have a trailing space here that I don't need and you're gonna see this in most real world companies the companies you're gonna work in in the future or maybe you are already working at usually in my projects I don focus so much on linters but it is good to know that they are a practice that should be used inside of your projects So what I going to do next is completely optional, but I think it's a very, very useful extension which can help you improve the quality of your code and gain a better understanding of which linters you need for your project. You can maybe even recommended inside of your company if you're having trouble deciding between some. So I'm going to save this like this for now. Remember, I added a little space here, just a trailing space. And what I'm going to do is I'm going to go inside of my extensions here. And I'm going to search for an extension called trunk. Go ahead and select the first one. It's called the trunk check. And go ahead and click install. Once you've installed it, leave it like this. And you're going to have a terminal pop up. This terminal is right now looking through your entire project and all the files, and it added all the necessary linters inside of its config file for your project. So after it's done, you're going to get redirected to this trunk.yaml file or the config file, and you can see that you can press any key to close the terminal. So just go ahead and press any key to close it. Let's take a look at this trunk.yaml file. As you can see, it scanned throughout our repository and it decided that we need two runtimes, Node and Python. It also decided that we need these linters to ensure the code quality and standard of the project. And if you want to, you can even go further ahead with this and you can go back inside of your terminal. I'm going to open a new terminal for now. And since you have trunk installed, what you can do is you can write trunk login like this. And this is going to give you a login link. And you can then open that inside of your browser. And if you want to, you can continue with GitHub. After that, you might have seen a little authorized button here. So just press on it and you're going to have a message you successfully signed into trunk CLI. Of course, this is not required for this project, right? And you don't even have to log in to use trunk. So all that you need is an extension. But I want to show you the website because if you want to, you can visit the documentation. And in here, you can read everything that Trunk can do regarding this check linter. So it has a lot more options. The CA debugger, CR analytics, even some merge queue right here. But we're only going to focus on the check functionality of Trunk. And as you can see, Trunk Check runs more than 80 tools in your repositories, which allow you to prevent anti-patterns and bugs, prevent misconfiguration or infrastructure code, and enforce code formatting and style, and much more. And you can follow this documentation if you want to add it to your repository, and you can configure trunk.yaml to whatever you want. So let me show you how this now looks inside of the code. So if you want to, you can play around with trunk, you can enable them inside of your repository, and you can even add GitHub Actions, which are gonna fire on every pull request to ensure that it can be merged according to the code standard and quality to prevent any bugs. So this is really good for production-ready websites if you want them. Perfect. So now that I've explained that, I'm gonna close these tabs right here and I'm gonna go back inside of my page.tsx file. And as you can see now, I have an error here, trailing whitespace, exactly what I told you before. So in order to fix this, I can just go ahead and remove that and save it like that. And now that I installed the package trunk, sorry, the extension trunk, you can see that I also have it right here in my sidebar. In your case, it might be on this side. And if you can't see it, you might have three little dots here, which will allow you to see hidden extensions. So when you click on this here, you can also look that you have some hidden issues here, like incorrect formatting. You can also play around with actions and run the following actions if you want. I'm not going to do all of that for this tutorial, but I just want to show you how cool this extension is. So let's see what incorrect formatting we actually have here and how we can format it. Well, what we can do is run Command-Shift-P or Control-Shift-P and run format document width like that and select trunk check. and as you can see now it added double quotes and it added semicolons at the end of my functions and now if I save this file and go back you can see that it has zero issues inside. So you can use trunk every now and then just to see the quality of your code and if there is anything you can improve in your production code. Perfect. So you can save that now and what I want to talk about now is how routing works in Next.js. So let's go inside of this app folder here. And let's create a new folder called test like that. And inside, let's create a new page called page.tsx. And let's go ahead. And this is what you're going to see me do a lot of times in the project, I'm going to write SFC, I'm going to press tab, and all of a sudden, I'm going to have this code snippet. So if you're wondering how I have that, it's another extension called Simple React Snippets. So go ahead and install Simple React Snippets if you want that. So let me show you again. All I do is write SFC and press tab. And then I can give my component a name, test page. Then I can press tab again. And now I am focused on the props, but we're not going to need them. And if I press tab one more time, I'm focused on the return function. And inside, I can just write a div, and I can write hello test page like that. Perfect. And you might be wondering, all right, so why did they do this? Well, we are learning how routing works in Max 13. So right now, I created a test folder inside of the app folder, and the test folder has a page.dsx file inside of it. That means that that page is accessible now. So I can go to slash test now. And as you can see, I'm going to see hello test page right here. So that is how routing works in Next 13. And one trick I immediately want to warn you of, every folder that you create inside of the app folder can potentially become a route. For example, let's say that you want to create a very simple components folder, right? Like the one we have outside of the app folder. But let's say for any reason you want to create it inside. Technically, you can do that. But watch what happens if inside you accidentally have a component called page. So I'm going to do a very same thing. I'm going to write component. So some component thing like that. And I'm just going to give it this is supposed to be a component, right? So if I created a components folder, obviously, this is my page component, right? Well, guess what? You can technically go to slash components now like this. And look at this, it's rendered as a route. That's not something you want to happen. So now you learn that every folder you create inside of the components folder is going to become a route. So here are a couple of ways to prevent that. So we're going to learn some new types of folder inside of the app folder. If you want this to not be routable, What you can do is rename this to underscore components like this. And as you can see, now I have a 404 page here, right? So for now, we can delete this underscore components folder like that. And if you have this error right here in page.es, that is just cache. So you can just save this file and close it. All right. And now I want to show you something else. So I'm going to go back here to the test page. So go to slash test here. and I want to show you how to create route groups. So route groups are another type of folder which you can write inside of the app folder which will have no effect on the URL. And route folders are, for example, parentheses like root, for example. As you can see, I created a new folder inside but if I create, so what I'm actually going to do is I'm going to move this main page.tsx where we have a delete button and I'm going to move it inside of root right here. and I'm going to save the file like that. And if I go back to localhost 3000, you can see that I still have that page as my root page. So why is this working? And what happens if I go to slash root, for example? As you can see, it's 404. It's not found. That's because the root folder is an organizational folder, meaning that it will not affect the URL. But one thing that it can do is have its own layout, which can then affect all the other routes inside of that component. So I'm going to keep this as it is now. And I'm going to delete this test page, for example. And I want to show you, for example, let's create a new organizational folder called alph, like this. And inside of it, I'm going to create another organizational folder called routes. And then inside, I'm going to create two routes. They're going to be login and they're going to be register like this. I'm going to create page.csx in both of them. So this is just going to be login page. And let me just return a div saying login page like that. I'm going to copy that and I'm going to create a new page inside of register. I'm going to paste it here and rename it to register page. and now how do you think we access these routes so we have login and register well remember what I just explained you with the root folder so when folder is inside of parentheses inside of the app folder that means it has no effect on the url it is purely organizational right so the way we can access those routes now are simply by writing slash login like this or slash register like this. So you're probably wondering, all right, well, what's the difference between the underscore folder and the parentheses folder? Well, the underscore folder completely eliminates the entire route and all of its content from the router. But the organizational folder with parentheses, it's just not part of the URL until it reaches a normal folder. And you might be wondering, well, why did I create two organizational folders here? Well, because that's what I'm trying to do. I trying to organize my structure in a better way because this organizational folder can also have another file inside of it called layout like this And as you can see now I have an error here So let quickly fix the error by going inside of layout and let's write root layout. And now let's extract the children from the props because every layout component has children. And just mark it as react.react node like this. And inside write a div and render children inside. And once you save the file, you're probably going to notice, well, our register page looks the same. My login page also looks the same. What's the difference? Well, nothing. But if you take a look at the HTML structure, you're going to notice that we now have one extra HTML, right? And to make this more noticeable, let's give this div a class name of h-full bg-red-500 text-white like this. There we go. Look at now. So as you can see now, my login page has a red background and it has white text. And same is true for my register page. Why is that happening? Well, that's why organizational folders are useful. So A, you can use them to organize your files in a better way if you don't want them to just be thrown around. And B, you can create layouts inside of organizational folders. And this layout is going to reflect all the routes inside. So both login and register now have a BG red and a text white. So we are going to use this throughout the project to create separate layouts. For example, when we do a landing page, we don't want to have a navbar and a sidebar, right? But when we are inside of our Notion clone, we want to have a sidebar and a navbar if we open the specific document. Well, we can leverage organizational routes and layout files in order to achieve that. Great. So we have now prepared this little structure here. You don't have to worry about this too much. We're gonna remove these folders, but I just wanna practice with you a little bit. So what I encourage you to do now is to try and understand how these folders work. So it's important that you understand when you see a folder in parentheses, it's not part of the URL until it reaches a normal folder. And then this is reachable at just a slash login. Same is true if I just have root and inside page.tsx, right? So this is now accessible at just a slash. It doesn't matter what the organizational folder is named. The only thing that matters is that it is an organizational folder and you can notice that using the parentheses. So I just wanted to cover different types of folders inside of the app folder. We covered the underscore folder, which we're going to use throughout this project when we want to add components which are not reusable. So all reusable components are going to be inside of our components folder where our chat cn button is right now. But for example, if I only want to create a component like a navbar in the landing page, right? There's no point in making that reusable. So what we're going to do is we're going to create the underscore components folder and put navbar inside of there. That way, we're going to ensure that that folder is nowhere rendered inside of our routes, but we can still use it as a component. I hope I made this clear enough. If you're confused, don't worry, we're going to go slowly and I'm going to explain it once again while I'm doing that. But for now, this is perfect. Great, great job. So what I want to do now is create our marketing page, or as we are going to refer to it, landing page. Before we do that, I quickly want to address that I added a class h-full in this root layout right here and the background color of red. But if you look at the browser, It seems like this is not taking up the entire space as you might expect with this H4 class right here. And that's because we are missing one thing inside of our app folder globals.css right here. We are missing a class name which is going to add height 100 to HTML element, to the body element and to the root element. So write HTML, body and comma colon root like this. and go ahead and write height 100% like that. And let's refresh. And there we go. Now you can see that our age full class, which we've given inside of this auth layout right here, is working and it's taking up the entire space. So what I want to do now is I want to go ahead and remove everything we don't need in here. So we created some folders to explain all the types of folders which we're going to work with. but we can for now remove the alt folder entirely. We don't need it. So you should get a 404 if you go to slash register or slash login. So you can just go to plain localhost 3000 where we have our delete button. And instead of calling this, whoops, instead of calling this a root folder, instead I'm going to rename this to marketing, right? So remember what I said, when a folder is in parentheses, it doesn't matter what it's called. It's not going to be the part of URL. So you can safely save this. And once you do these changes with the root pages, you're often going to get this weird unsaved change inside of the .next folder, which is the cache, right? So you can fix this very easily. Just click inside of the file and save it just like that. If you mess it up, you can always completely remove this .next document. So if you think you're having some problems with the cache, let me show you how you can fix that. So you didn't have to do this if everything is working correctly for you, but I just want to show you a quick fix for it. So I'm just going to close this terminal. So if you encounter an error with the cache, you think you've messed it up, no worries. You can just do this. You can do rmrf.next. So just remove the next folder. Or if you're using Windows and this command doesn't work for you, you could have also just right click on it and delete, right? And then what you're going to do is just npm run dev. That's it. And we'll take a look now. You're going to see that I have a new .next folder generated right here. So it's that simple to fix that if you think you've messed it up. So no worries. Just remember, refresh your local host once you do this. Great. So what I want to do now, make sure you rename this to marketing. And we're going to leave it like that for now. And instead, let's go inside of our layout.tsx. And what I want to do is I want to change this metadata. As you can see now, my tab has a title of create next app. And I want to change that so it actually uses the name of the app we are going to create, which we're going to call Jochen. So let's give it a type of Jochen like this. That's going to be the title. Let's give it a description. Well, you can do whatever you want. But for example, an app like this would have something like the connected workspace where better, faster work happens. Something like that. It really doesn't matter. Great. And what I want to do now is I want to show you how to add, how to change this little icon that we have right here, also called a favicon. As you can see right now, it's showing a Vercel icon here. And the reason it's showing that is because you have a favicon.ico inside right here. So if you technically remove this icon and refresh your page, as you can see now, it's gone because it cannot read any favicon. But you don't have to always put a favicon.ico inside of the app folder to add a favicon. You can also keep it wherever you want, for example, in the public folder. But you do have to tell layout metadata where it's located if it's not inside of the app folder. So that's exactly what I want to do now. So I want you to go inside of my GitHub where I have my logos and images, right? Or you can, of course, find any logo online you want. So go inside of my GitHub and go inside of the public folder. And you see I have a lot of images here that you are going to use. But the ones I want to focus on now are the logo.svg and logo-dark.svg. So I'm going to copy logo.svg and I'm going to click download raw file like that. and I'm gonna go ahead and drag and drop that file inside of my public folder. So I open the public folder here and through Chrome, I'm just dragging and dropping it into the public right here. And I have a little extension which previews that logo here. So it's just an SVG file. And go ahead and do the same thing still inside of my public folder with logo-dark. So this is a logo which we're gonna use if we are in dark mode, right? So let's go ahead now and let's drag and drop that as well in the public folder here. So logo dash dark and drag it in the public folder. So it's together with logo dot SVG. Perfect. Now that we have that, I'm going to go back inside of my app folder layout right here and I'm going to add icons object and I'm going to write icon to be an array like that. Object media. and we're going to write if in parentheses refers dash color dash scheme to be light. So if we are in light mode, the URL is going to be slash logo dot SVG. And the href is also going to be slash logo dot SVG. So when things are in the public folder, you don't have to write public. They are immediately under the slash, right? So the reason I added two icons is because as you can see in my tab, I'm using dark mode on my Chrome and it will be weird if the icon was dark as well, right? It will barely be visible. So I want to use light icon if I am on dark mode, but a dark icon if I am on light mode. So that's why we're going to add an array of icons here. So you can just copy and paste this below and change this one to be prefers color scheme to be dark. So if we are on dark mode, in that case, we're going to use logo-dark.smg. And there we go. Look at my browser now. See how it says Jochen and see how I have a nice little icon, which is right this one. It's using the dark one right now because I'm in dark mode by default. But if I switch my Chrome to light mode, it's going to be using this one. So it's going to look better because the tabs are going to be white. Perfect. So we have that solved. So I just wanted to do that. And what I want to do now is I want to go inside of the marketing and I actually want to start creating the marketing page. So let's go inside of marketing page.vsx here. And I want to go ahead and change this to be constant marketing page like that. And I want to export default marketing page. So I just changed the way I export default. It doesn't really matter. And let's see the mistake I made marketing. Oh, I missed an page. All right. So marketing page, make sure you don't have any type.\nAnd what I'm going to do now is modify this. So we don't need the button at this moment. Instead, let's add a div with a class name of min-h-full flex-call like this. So we're creating our outmost div, which has a min height of 100%. And we already prepared that this is going to be a flex box. The reason we prepared that it's going to be a flex box is because we're also going to have a footer, which we want to keep all the times at the bottom. And the way we can do that is by using flexbox. So when we create our footer element, we can just push it all the way to the bottom using flex. Perfect. So what I want to do now is a div inside with some other class names. So let's go ahead and let's write class name to be flex, flex-call, items-center, justify-center. On medium devices, it's going to be justify-start. Text is going to be center. Gap of each item inside of this div is going to be separated by the value of eight, which is two RAM or 38 pixels. Flex is going to be one. So this is what I was talking about. So this div is going to take majority of the screen. And then below that, we're going to add a footer. So the footer is always going to be at the bottom because of this flex one option right here. And because the outer div has the flex layout. Perfect. And let's also give it some padding. So from both sides, padding six, and from the bottom padding 10. Perfect. And inside, first thing I want to add is a heading component. So if you save, you're going to get an error because heading component does not exist yet. So let's go ahead and let's see how we're going to do this. So let's go inside of the marketing folder. And remember what I told you, we could technically create the components folder just by writing components. But remember, that is going to be part of the URL. And we don't want that. So what we're going to do is create it with underscore components like that. And you might be wondering, well, why not just reuse this components folder outside of the app folder? Well, I want to keep that components folder for reusable components like buttons and dialogues, models, other stuff we're going to have. But whatever I write in this components folder, it's only going to be for the marketing or for the landing page. So it doesn't make sense to share it with the other files. So let's go inside and let's add our heading.tsx component. And let's go ahead and let's mark this as use client. So this is not going to be a server component. It's going to be a client component because we're going to have some actions on our buttons inside. Great. So let's go ahead and write export const heading like this. and let's go ahead and let's return a div with a class name of max w3 excel so we limit how wide this heading can go and let's give it a space dash y dash four so all elements inside are evenly spaced out perfect now let's write an h1 element and we're going to write your ideas documents and plans unified. Welcome to, and then we're going to write a span element, and we're going to write Joshin. And let's give this span element a class name of underline. Now let's give this, well, before we do anything, let's actually go back to our page.tsx and let's import this heading so we can actually see what we're doing. So make sure you import heading from dot slash underscore components slash heading like this and go ahead and save this and there we go you can see how I have this text right here in the center now and now let's go ahead back to this h1 element and let's go ahead and let's give it a class name of text dash 3xl on small devices is going to be text dash 5xl and on medium devices is going to be text dash 6xl so we're going to have different size of text depending on how wide our screen is. And we're also going to give it a font bold on all devices like this. So currently we are on mobile mode, right? But if I expand, you can see that it looks much different. Great. And let me just reset my zoom. So it's in something expected like this. There we go. So it's like that. Now below the H1 element, let's add an H3 element and inside we're going to write joshin is the connected workspace where and let's manually add a little break here better faster work happens like that so you can see we have a much smaller text below this h1 element and let's give this h3 element a class name of text-base let's give it a small device of text-XL. Let's give it a medium device of text-2XL. And let's write font-medium. Great. And now what I want to do, well, we can actually leave it like this for now. Below this H3 element, the only thing we can actually do is add a little button here. So we can already prepare. So just add a button and import that from button as slash components UI button. And we're going to write enter Jochen like that. And let's also write a little arrow right icon from Lucid React. So you already have Lucid React installed when you installed ChatCN UI. So make sure you import arrow right from Lucid React. And let's go ahead and give it a class name of H-4, W-4, and ML-2 like that. Great. So you can see that now we have a little heading here. We have the description and we have a button here. Of course, this is later going to become dynamic depending if we are logged in or not. All right. So now that we have our heading, we can go back inside of the marketing page. And below the heading, we can now add a new component called heroes. And inside of heroes, we're actually going to add all of our nice images. Let's go ahead and create the heroes component the same way we did with the heading component. So instead of the underscore components, create heroes.tsx. And let's go ahead and let's import image from next slash image. And let's export const heroes. And we can just do a quick little return function with a div saying heroes like that. Go inside of page.tsx and import heroes the same way you did the heading. And go ahead and save this now. And now we have a text here which says heroes. What I want to do now is I want to add the images we need inside of these heroes. So prepare your public folder the same way we did with the logos, right? And go back inside of my GitHub and go inside of my public folder. And the ones we need right now are documents, documents-dark.png. And we also need reading and reading-dark.png. So first, let's do the documents. So documents.png right here. And let me just expand this so I can find the download button like that. And I will drag and drop it inside of my public folder. So now I have documents. And now let's use the documents-dark the same way. You can see how this one is white. So it can look good against a black background. And do the same thing with reading. So reading.png here. Let's download that as well. Let's pass it inside of the public folder. And let's also copy the reading dark dot png. Like that. And let's move it inside of our public folder. Perfect. So just make sure that you have the document, the sorry, the documents, documents dark, and that you have the reading dark and reading. Great. So you can go back inside of your page now. And I'm going to expand my code screen here. and I'm gonna go back inside of my heroes component, which I just created inside of the marketing underscore components heroes right here. Let's give this div a class name of flex, flex-call, items-center, justify-center, max-w-5xl. And inside of that, create another div with a class name of flex-items-center. And inside, create a div, which is going to hold our image. So class name, relative, W, open square brackets, 300 pixels, H dash square brackets, 300 pixels as well. On small devices, the width is going to be 350 pixels. And on small, height is going to be 350 pixels as well. On medium devices, height is going to be 400 pixels. and the medium devices width is going to be 400 pixels as well. And inside, you can now render image component, which we imported above from next slash image and give it a source of slash documents.png, give it a property of fill and give it an alt of documents. And let's also give it a class name of object dash contain like that. There we go. So you can see how we have a nice little documents image here. And now what I'm going to do is I'm going to go outside of this div, which is wrapping this image component, and create a new div like that. And let's go ahead and write a class name here. Relative H-400 pixels, W-400 pixels as well. and inside render another image component with a source of slash reading.png, a fill property, and a class name of object-contain. So you might be wondering, well, this looks a bit weird. Well, I only want to show you how this will look because now we're going to go back inside of this. Let's just see what's missing for the image. Sorry, we are missing an alt property. So let's give it an alt property of reading. There we go. And now go back inside of this div, which is encapsulating the reading image, and hide it on mobile devices. So hidden by default. But when it reaches a medium, it's going to be blocked. So you can see on mobile, we're not going to render it because we don't have that much space to work with. But when we expand, you can see that it's going to come back to life here. So that's what we want. All right. And now what I want to do is I want to go back inside of page our marketing page where we have a heading and the heroes And below that let add a footer component So the same way we did with the heading and the heroes. And now let's go back inside of the components folder and create a new file footer.tsx. and go ahead and export const footer and return a div saying footer. Go back inside of page.tsx and render the footer from .slash underscore components footer. And you can see all the way at the bottom here, we have our footer text. We can now go back inside of the footer component. Let's give it a class name of flex items-center w-full padding of six Let's give it a BG of background. And let's give it a Z-50. So we have a Z index here. And now I want to create a logo component. So you can see when I save now, we're going to get an error because it doesn't exist. But we're going to create a logo component, which is going to be shared across the footer and across the navbar, which we are yet to create. So let's go. And inside of this components folder, where we have the footer, the heading, and the heroes, create a new file called logo.tsx. Go ahead and import image from next slash image and go ahead and import pop-ins from next slash font slash google. So our logo is going to have a special font and go ahead and import cn from as slash lib utils. So you got this when you installed chat cnui and the cn library is going to be used to dynamically append classes to our tailwind elements without the fear of overriding or incorrect merging. So yes, you could technically do it with template literals, but using this library is a better and the proper way of doing it. You're going to see what I'm talking about in a second. But before we get there, let's actually define this poppins font. So const font is equal poppins, open an object, subsets, open an array and write Latin and give it a weight of 400 and 600. And now export const logo and return a div with a class name of hidden. So on mobile devices is going to be hidden. On medium is going to turn into flex. Items is going to be center and gap between the two items is going to be two. And now let's add the image component. And let's write source to be slash logo.svg, which we already have. Let's give it a height of 40. Let's give it a width of 40 as well. And let's give it an alt of logo like this. And what I want to do is go back inside of footer. And you can now import that from dot slash logo because they are in the same folder. And if you now expand your screen, you can see little logo here at the bottom. On mobile devices, it's hidden because we don't have that much room to work with. But on large devices, it's right here and visible. Great. So let's go back inside of this logo because we're not done here. And let's add a paragraph here saying Joshan. And let's go ahead and let's give this paragraph a class name. And it's actually going to be dynamic class name. So go ahead and write cn. And let's write font-semi-bold. and then let's append this dynamic class from this font. So font.className. And now if you expand your screen, you can see how we have a nice little joshin here. So if you cannot see this, make sure you expand your screen or you zoom out to a specific level so you can see it. All right. So now that we have this, what I wanna do is I wanna go back inside of the footer component. below the logo here, create another dip with a class name of MD ML-Auto, W-Fool, Justify-Between, MD Justify-End, Flex, Items-Center, Gap-X-2, and Text-Muted-Foreground. and inside we're going to render two button elements. So import button from add slash components UI button as I did right here at the top and I'm just going to keep this two separated and in here I'm going to write privacy policy. I'm going to give it a variant of ghost and the size of small and you can copy and paste this and you can change this one to be terms and conditions and there we go now you have two little items here of course we don't actually have privacy policy in terms and conditions but if you want to use this for your real website I just prepared this for you here and you can see that when you collapse they are each on its own side and we don't have the logo but when we are on desktop mode they are all the way to the right here perfect so now that we have this done we are finally ready to create our navbar so let's go and create a layout for our marketing page so I'm going to go inside of the app folder inside of this marketing folder right here let me just expand my screen a bit so I can resize the navbar here and besides the components and page.tsx I'm going to create a new file called layout.tsx because remember each organizational folder can have its own layout and in this case our marketing layout is going to have a navbar. So let's go ahead and let's write a marketing layout. Let's extract the children. Let's give this children a type of react.react node. And let's go ahead and let's return a div, the class name of h-full. And let's go ahead and return a main element where we're going to render the children. And in the main element, add a class name of h-full and padding top of 40. And above that, add a navbar component, which if you save right now is going to give you an error. So let's go inside of our components folder and create a new file navbar.tsx. And let's go ahead and mark this as use client. And let's export const navbar. And let's return a div saying navbar. And let's go back inside of layout.tsx and let's import it from .slash components navbar like this so we no longer have that error. Great. Now we can go back instead of navbar.tsx and what I want to do is I want to create a hook called use scroll top. So I want our navbar to not have any border while we are not on the top but if we are zoomed in like this and if we scroll down I want the navbar to follow that and add a little border to it. You're going to see in a second what I'm talking about. So let's go ahead and let's create that hook. So we are going to create a new folder called hooks at the root of our application. So you can just click on a random file like this. Just make sure it's not a folder. Click on a random file outside of any folder here. And then you can create a new folder at the root called hooks like this. Go inside and create a new file called use-scroll-top.dsx. And let's go ahead and let's import useState and useEffect from React. Let's export const useScrollTop. Let's give it a threshold to be a default value of 10. Let's add a state, scrolled, setScroll. So we're going to detect whether the user scrolled or not and use state by default is going to be false. And now let's add a use effect, which is going to have a listener here. So we're going to write window.addEventListener on scroll. And we're going to pass in a handle scroll function, which we don't have now, but we're going to have in a moment. And we also have to write a return function, which is unmount where we remove the event listener. So window.removeEventListener, scroll, and the same one, handle, scroll, which we don't have yet. And then go inside of useEffect and write const handle, scroll, like that. And write if window.scrollYPosition is larger than threshold, we're going to assume that user has scrolled. So we're going to set the state of scroll to true. Otherwise, if user goes back to the top, we're going to set the state of scroll to false. So this is going to be just a little trick so we have better user experience. And let's also pass in the threshold prop in the dependency array. And let's go ahead and let's return scrolled. So this state value right here. Great. And now we can go back inside of our app marketing components, navbar.tsx. And let's go ahead and let's add that scrolled. So const scrolled is going to be use scroll top from add slash hooks use scroll top. And now in this div, we're going to add a class name, which is going to be dynamic using the CN library from add slash lib slash utils. And let's go ahead and first let's give it some default classes, which are not going to be dynamic. So Z index of 50, BG background, fixed, top 0, plex, items-center, W-full, and padding of 6. Great. And then inside, sorry, then after this default classes, add a comma. And if we have scrolled away from the top, only then are we going to add a border bottom and a shadow like this. So only when we scroll are we going to have a little border. So let's see if we can already do that. When I scroll, there we go. You can see how I have a nice little shadow here. But when I'm up to the top, you cannot even distinct that this is a separate component from the background. But when you scroll, see how you have a nice little effect here. So it's details like this that make the landing page better. Great. So now that we have that I going to replace this navbar with a logo component from logo which we created like that Perfect And let just see Okay so it right here Perfect Great. And now, besides the logos, I'm just going to zoom this out. So I'm in desktop mode. Make sure you're in desktop mode as well so you can see your logos. And beside the logo here, I'm going to go below it and add a new div with a class name of MD ML-auto, MD justify-end, justify-between on mobile devices, of course, W-full, Plex, items-center, and gap-x-2. And all I'm going to write here for now is going to be login like this. So we don't have the button for that yet, but I just want to show you that it's going to be here or on mobile, it's going to move to this side. Perfect. So now that we have that, what I actually want to do is I want to enable light and dark mode for this landing page. So in order to add dark mode, I'm actually going to go ahead and follow the Shatsian instructions for it. So go to Shatsian website and click on components here. And then you can find this tab which says dark mode. Go ahead and select next JS. And let's now follow the steps which we need to do. So first one is to install next themes package. So I'm going to copy this for NPM because that's what we are using. And let's go inside of the terminal here. I'm going to open a new terminal and I'm going to paste NPM install next-themes. And let's go ahead and do that. Perfect. And then what we're going to have to do is create a theme provider. So that is step two. So go ahead and copy this entire code. if you for any reason cannot find it on the Shatsian website, you can always find it in my GitHub. So I'm actually going to put it in a different space. So I'm going to go inside of components and I'm going to create a new folder called providers. So I just want to separate the providers from the rest of the components. And inside, I'm going to create a new file team dash provider dot tx and go ahead and paste everything inside. So if for any reason you cannot find this on the chat scene documentation, you can just visit my GitHub and go ahead and find the components, providers, team provider, and just copy it from there. Perfect. So now that you have that, the step three is to wrap the team provider inside of our layout. So let's go ahead and let's do that. So we're going to be doing that inside of the root layout. So let's go inside of the app folder layout.tsx. So not the marketing layout, the app folder layout.tsx, where we have our title, description, and dynamic items here. And let's go ahead and let's just collapse this children inside. And let's just wrap this in theme provider. And now we can see that there are two imports, you can either import it from next slash themes, or from as slash components providers, team provider. You have to use this add slash components providers team provider, the one we just created. Otherwise, it's not going to work. So go ahead and copy that and save it like this and make sure you have the team provider from add slash components providers team provider. And what we have to do now is we have to give it some attributes. So go ahead and give it an attribute of class. Let's give it a default theme of whatever is the system theme. Let's give it enable system option as well. Let's give it disable transition on change prop and storage key is going to be Joshin dash theme like this. So you can do whatever, you can write whatever you want. It's for example, you can put dash two, it doesn't really matter. Just make sure it's unique. And I think that should actually be it. One more thing I want to do is go to the HTML and I want to add suppress hydration warning like that because changing the theme can cause some hydration warnings which are not really important to us so we can suppress them with this prop to the html tag great so now as you can see when i refresh my dark mode is activated for you that might not be the case so don't worry if it's not i use dark mode on my system by default and since we added the default theme to be system and enabled the system and I added a storage key that used my system theme. So don't worry if yours is not working at the moment because now we're going to add a button in the nav bar so that user can click and change the theme that they want. So we have to do that by going back inside the Shats in UI. We just did the step three right here and now we have to do the step four, add a mod toggle. So let's go ahead and let's click on code. So this is what it's going to look like, right? And now let's go ahead and go inside of code here and just copy this entire thing and go inside of your components folder right here and create a new file mode-toggle.tsx. So let me show you exactly. So I just created it inside of the components folder. So not inside of UI, but inside of mode.toggle and just paste the entire thing inside. And as you can see, now we have an error here because we cannot find this drop down menu. That means we have to install the drop down menu from ShatCN. So let's quickly go back inside of our terminal here. And let's go ahead and I'm just going to zoom out. Actually, I'm not going to zoom out. I'm going to close this terminal and I will shut this down and go ahead and write the following npx chat cn sui at latest add drop down dash menu like this so we have to install that package in order to have our mod toggle working so now it's installing the drop down menu let's give it a couple of seconds and once it's done you can see that you're no longer gonna have the error inside of the mod toggle so i'm just gonna run my app again and there we go you can see that now there are no errors here because this exists. So if I go inside, you can see that I have all the drop down code here. But this is what we are focusing on the mod toggle. Make sure you save this file, make sure it's named mod toggle, make sure it is inside of the components folder. And now what you can do is go back inside of marketing components, navbar.vsx right here. And instead of this login text add a mod toggle from add slash components mod dash toggle like this perfect and I'm just going to move it to the top with the others great and now if you go back inside of your application and refresh instead of this there we go I have a little moon icon and I can change this to light mode now or I can change it to system so now we have enabled light and dark mode but we're obviously missing something. When we switch to dark mode, we cannot see our pictures and some things just seems weird, right? So let's go ahead and let's resolve that now. First thing I want to resolve is this logo right here in the navbar and in the footer. So we have to find that component. Let's go and close everything and let's go inside of the app folder marketing components logo.tsx right here. And what I'm going to do is I'm going to give this image a class name of dark hidden. So on dark mode, the logo is going to be hidden. And then I'm going to copy and paste this just below like this. And I'm going to change this source to be logo dash dark. But this one is going to have a different class name. It's going to be hidden by default. But if it's on dark mode, It's going to be blocked like this. And let's take a look at it now. And there we go. You can see how now on dark mode, we have the opposite white logo right here, both in the footer and in the nav bar here. Great. But now let's just go ahead and quickly revisit our nav bar here. And I want to give it a different background if we are on dark mode. So let's go ahead and let's give it a dark property. So if we are on dark mode, let's give it a background of open square brackets and write a hex code of, whoops, just a second. My apologies. 1F, 1F, 1F, like this. And there we go. You can see that now our navbar has a slightly different color than the rest of the page. So we're going to reuse this. You can actually copy it like this. And now we're going to add it to some other stuff that we need, like our layout. So let's go inside of our layout, but only the layout for the marketing. So go inside of this layout. We have the H-full right here. And go ahead and also paste the dark background to be 1F, 1F, 1F. There we go. Now you can see that the knob bar matches the background of the layout. And now we have to do that same thing, but for our footer. So inside of components, go inside of the footer here and just add this to the end. And there we go. Now our footer matches the color. Perfect. And you can always see how the images are becoming a bit more clearer, but that's not what we want to do. What we want to do is we want to use different images depending on the light and dark mode, the same way we did with our logos. So what we have to do is we have to go inside of our heroes component. So go inside the underscore components, heroes.tsx right here. And let's find this first, documents.png. And besides the object-contained class, I'm also going to give it a dark hidden class like that. And then I'm going to go ahead and copy this below. And I'm going to change this one to be hidden by default. But on dark mode, it's going to be block. And instead, I'm going to use documents-dark.png. And now, as you can see, this color matches the, it's more visible in dark mode now. And if I switch to light mode, you can see how it reverts back to the dark one. So let's continue working in dark mode and let's fix the issue for this icon right here. So below that, we have the reading.png. Let's go ahead and let's copy and paste this. Let's change this to be reading-dark.png. let's give this first one a class name of dark hidden and let's give this one default hidden and dark to be blocked and now if you go ahead and expand your screen there we go you can see how better this looks now and if you switch back to light mode there we go all of it looks beautiful as well great great job so now that our landing page is almost finished what I want to do is connected to our back and then our database And I also want to add authentication In order to achieve those two things we going to be using Convex as our back and as our real database and we going to be using Clerk and connect it with Convex for our authentication. So what I want you to do first is create an account with Convex. So go ahead and find the login button right here, and just click log in with GitHub like this. So make sure you create an account and then you're going to be redirected to the dashboard. As you can see right now, I already have a project here, which I've used to develop this application. But what we're actually going to do is we're going to create a new project. So you can try and click here, but you're just going to get a pop-up to follow how to create a project in one of the quick starts right here. So you can either click on the link like this, or if you're having trouble finding this, don't worry. So I'm gonna show you how to do it from their website. So you can just go ahead and go on developers, go on documentation right here and find the quick starts right here. And once you're in the quick starts, go ahead and select Next.js as your language. So we already have created our app. So no need to do this step. What we have to do is npm install convex. So let's do that first. I'm gonna go inside of my terminal here and let me just go ahead and clear everything. And I'm gonna go ahead and run npm install convex like this. And after convex has been installed in our project, we're gonna run a command npx convex dev. And as you can see, this will prompt you to log in with GitHub and also to create a project. And it's also gonna add some stuff in our environment variable. So we don't have to do that ourselves. So copy this command npx convex dev and go ahead and paste it here. So npx convex depth. And for you, it might be a little bit different. I'm not sure if you're going to get this question immediately. Perhaps you're first going to need to log in using GitHub. So if you see any links inside of your terminal, just click on it and log in with GitHub. If not, if you're seeing this, what I'm seeing, just go ahead and choose new project. So this preselected option. Now it's asking us for a name. And you can see that it already pre-filled the name with the name of my folder. So it's Notion clone. Correct. And now just press enter and there we go. It created an entire project for us. And now we can see that you can just follow this link right here. So just click it and open it. And it's going to open that dashboard which you just saw a moment ago. So now if you take a look in your team, you're going to have one project. So this is my old project and this is the new one which I just created. And as you can see from this dashboard, you're going to be able to see all of your data, all of your files, your functions, your logs, your history, everything, and your schema once we create it. So it's going to be very useful to follow this dashboard along. What I want to show you now is what this npx convex dev command, so this which we just run right here, which created our project, you actually don't have to stop this terminal, right? In order to use convex, you're going to have to keep this running alongside your npm run dev, but we're going to get to that later. So for now, just keep this running in your terminal. And let's take a look at everything new we have now. As you can see, we have a new convex folder and we have .environment.local. So inside of this .environment.local, as you can see, we have the convex deployment ID right here, and we have the next public convex URL. And we also have a convex folder inside. And inside of it, we have some configs, we have some generated stuff, but we're not going to worry about that too much. what you need to know that inside of this convex, we're going to create our API functions, our queries and our mutations. Perfect. So now that we have this set up, what I actually want to do is go back inside of the convex page, go inside of the documentation here. And if you want, you can explore for yourself how to set up a clerk authentication. But don't worry, we're going to do it together. So what I want you to do is go to clerk.com or use the link in the description and just go ahead and log in. After you're here, just go ahead and create a new application. In my case, this is going to be Notion-Tutorial like this. And you can play around with how you want your users to sign in. In my case, I'm going to disable the email. I'm going to disable Google. And instead, I'm just going to select GitHub. If you want to, you can do it exactly like that. Or of course, you can select whatever you want and just click create an application like this. And now that you have these two items right here, go ahead and immediately copy them. And while we are in this environment file, let's also paste them just below this next public's convex URL. So make sure you have the next public's next public clerk publishable key and the clerk secret key like this. Great. What we have to do next is go back and just revisit the convex documentation. So where am I right here? So convex with clerk. So we signed up for clerk, we created an application clerk, and now we have to create a JVT template. So let's go ahead and do that. So go inside of clerk right here. And as you can see here in the sidebar, I have JVT templates like this. And let's go ahead and let's create a new template and select convex right here because that's what we're using. Leave it a name to be convex like this and you can let's just see if we have to modify anything. I think we can just leave it right here. So in the JVD section okay no not just new template and choose convex and we have to copy the issuer URL. All right so let's go ahead and do that. Let's go inside of our clerk and we have the issuer right here and just go ahead and copy this. So find the issuer input and click copy and then click apply changes like this. And there we go. Now, as you can see, you have a new convex JVT template right here. And let's see what we have to do next. So we have to create out.config.js inside of our convex file. So let's go ahead inside of our convex folder and create a new file out.config.js. And let's go ahead and let's export default providers like this domain. And in the domain, let's paste our issuer URL, which we just copied from the clerk.jvt template. And our application ID is going to be convex. So just make sure you don't misspell any of this stuff. If you want to, you can copy it from here as well. So it needs to be providers, which is an array, which has an object. The domain is our clerk issuer, which we copied from here, right? And our application ID is the name of our JVT template, which we left to be context. So it needs to match application ID, make sure both I and D are capital like this, and you can just save out.config.js. So now that we have this out.config.js, just make sure that inside of your terminal, you have this running, right? So as you can see, for a second, I had an invalid out convic. I think that's because I misspelled application ID. But after that, it looks like everything is okay. So if you want to, you can shut down the app and run npxconvexdev again. So after you've created this out.config.js file, just run npx convex dev again and confirm that there are no errors in this function. There we go. Everything seems okay. And that means it's successfully connected to convex. No errors. Perfect. Now we can close clerk. We can close this dashboard. And let's go ahead and see what we have to do now. So as you can see, we deployed our changes right now by using this. We confirm that there are no errors. And now we have to install clerk. So let's go ahead and open a new terminal now. So make sure you have the npx convex running. And now let's go ahead and let's do npm install at clerk slash clerk dash react like this and go ahead and press enter. And once that is finished, we're going to go ahead and we're going to create our convex provider and we're going to combine it with clerk provider. So let's go ahead and let's close this. Just make sure you have npx convex dev running in the other terminal. Go inside of the components folder, providers, and create a new file called convex-provider.tsx. So let me just expand this so you can see convex-provider.tsx. Mark this as use client like that. Go ahead and import React node from React. Go ahead and import convex React client from convex slash React. Go ahead and import convex provider with clerk from convex slash React dash clerk. Go ahead and import clerk provider and use out from add clerk slash clerk react like that. Now define the convex constant. So const convex is going to be new convex react client and inside passing process.environment.next underscore public underscore convex underscore URL. So if you check your environment.local you have that right here. Next underscore public underscore convex underscore URL. You can copy it from here and paste it again here to confirm that you didn't do any misspellings. And we have a little error here. So you can just put an exclamation point at the end for it to go away. Great. And now let's export const convex client provider like that. Let's go ahead and whoops. Let's go ahead and let's extract the children from here. So the children are a type of React node. And let's go ahead and let's return a clerk provider. And let's give it a prop publishable key to be process.environment.next underscore public underscore clerk underscore publishable underscore key. And the same thing, you can go ahead and inside of your .environment.local and just confirm that you have that here. Next, underscore public, underscore clerk, publishable key. You can copy it from here and paste it here to confirm. And just put an exclamation point at the end so there are no errors. And if you don't have this, well, you should just get it from clerk.com. So go inside of clerk.com, go inside of your dashboard,\nGo ahead and select the application which we just created. And there we go. Next public clerk publishable key. So just make sure you have that in your environment variables. Great. And now that you have that, let's go back inside of the convex provider. And let's go ahead and let's give it a children of convex provider with clerk like this. let's give it a prop use out to be use out which we imported right here from clerk react let's give it a client of convex which is this constant which we defined right here and then inside of that we can just render the children and now our entire application is gonna use authentication using convex as a database and clerk as the auth provider. And now what we have to do is go inside of ad folder layout.tsx right here. And just above the team provider, you're going to go ahead and import that convex client provider from ad slash components provider convex provider and just wrap the entire team provider inside of it like this. So make sure you have this imported and there we go. So now if you refresh you should not see anything because we have never started the app. So inside of my terminal I have one terminal which is running npx convex dev and in my another terminal I'm gonna run npm run dev so I can finally see my application. So let's just refresh this to confirm that we don't have any errors. And there we go, seems like we don't have any errors. And what we can do now is we can finally use some hooks to check whether we are authenticated or not. And we can also fire models to actually log in using GitHub in my case, or whatever you selected in your case. So let's go ahead and do that now. So first place I want to add authentication check to is the nav bar right here. So let's go ahead and let's find our app folder marketing underscore components navbar.tsx right here. And besides using the use scroll top hook, we're also going to be using const use convex out like this. And you can import use convex out from use convex out from convex slash react like this. And I'm just going to separate this imports. Great. So now we have use convex out here. And from here, we can extract is authenticated and is loading. So now what I want to do is I want to go outside above this mod toggle right here, and I'm going to add if is loading. In that case, I'm just going to go ahead and render a paragraph saying loading like this. And below that, I'm going to go ahead and I'm going to write if we are not authenticated by adding the exclamation point at the end of the boolean and if we're not loading in that case i'm going to render a fragment like this and i'm going to render sign in button which you can import from clerk from clerk react so go ahead to the top and let's go ahead and Let's import sign in button from at clerk, clerk react. So we have the sign in button now like this. And inside of it, we're going to render our button from at slash components UI button. So make sure you added the button from at slash components UI button. I'm just going to move it here. All right. And inside of here, I'm going to write log in like this. and I'm also going to give it a variant of post and the size of small. And I'm going to give this sign-in button a mod of model like that. Perfect. And then below that, I'm going to copy and paste this. So inside of this fragment, I just copied and pasted this entire sign-in button and we're going to do the very same thing, but this one is going to say get Joshin free like that. and it's not going to have a variant ghost like this. And now if you click on this, you should have, as you can see, a GitHub login model open. Perfect. So we have that working now. And as you can see, this is how it looks like on mobile. Perfect. So what I want to do now is I want to modify this loading a little bit. So if you refresh, you can see how I have a loading text here for a couple of seconds. Instead of having that loading, I actually want to have a little spinner here. So let's go ahead inside of our global components folder and let's create a new file spinner.tsx like this. And I want to import loader from Lucid React. And I want to import CVA and type variant props from class variance authority. and I also want to import cn library from lib utils. So the reason I'm importing the CVA from class variance authority is because I want to create some different variants for this spinner, right? I'm gonna reuse this spinner a lot of times in the project. Sometimes I'm gonna want it to be a little bit bigger. Sometimes I'm gonna want it to be a little bit smaller. So the best way to do that is using class variance authority. So let's write const spinner variance. to be CVA. And let's first give it some default classes. So that's going to be text, muted, foreground, and animate-spin. So that's going to be the default classes of the spinner. And then we're going to open an object and write variants like this. And we're going to add size as a variant. Default is going to be H-4 and W-4. Below that, we're going to add a small option for it. So this one is going to be H-2, W-2. Below that, we're going to create a large option, which is going to be H-6 and W-6. And below that, we're going to add an icon, which is going to be H-10 and W-10. Now we're going to define which one of these is default. And well, you can already guess by name, but we have to manually select that by adding default variants, size, and it's going to choose default. So this is going to be the default size of our spinner whenever we add it. So we don't have to manually add the spinner variant every single time. And now let's create an interface, spinner props to be extends variant props like this. Open this pointy brackets and write type of, let me just expand this. It's going to be type of spinner variance constant, which we have defined right here above. Great. And you can just go ahead and open an empty object at the end. And now go ahead, whoops, where did I go? And now, go ahead and export const spinner, which will take the size as a prop from spinner props, like that. And go ahead and return a loader, which we imported from Lucid React, and give it a class name to be cn, spinner variants, and pass in the size object. There we go. So we have a nice little modular spinner component here. Save this file. And now we're going to go back here where we currently have the loading. And instead, we're going to render that spinner from add slash components spinner like this. So just make sure you imported the spinner. I'm going to move it with the others. And now if you take a look, when I refresh here, you can see how I have a nice little spinner here for a second instead of that weird instead of that weird loading text great so what I want to do now is I want to display a different state if we are actually logged in right so let's go outside of this if clause right here so at the bottom just above mod toggle and let's write if we are authenticated so this time without an exclamation mark if we are authenticated and if we are not loading in that case go ahead and open a fragment again and let's just add a button element inside let's give it a variant of ghost a size of sm and as child prop and pass in a link from next slash link so just make sure you import the next slash link like i did right here i'm just going to move it to the top and go ahead and give this an href of slash documents which is a site we don't have yet, but we're going to have in the future. And just write enter Jochen like that. And outside of this button, we're also going to have a user button. So we don't have this user button yet. So let's go right here where we have the sign in button and let's add user button as well. And one important thing is that in this user button, you give it a prop after sign out URL to be a slash and save this document. And go ahead and refresh this one more time. And now let's try our login here. So I'm going to continue with GitHub like this. All right. And there we go. As you can see, I'm right here and I have my user button from here. And now it says enter Joshin. Perfect. So what I want to do now is I want to modify this hero action here as well to also tell me different things depending on whether I'm logged in or not. So before we do that, I just want to show you that from here, you can sign out, for example, you can log in back again with GitHub, right? Or you can also go ahead and manage your entire account, change your picture, log out, add different accounts, delete your entire account, modify some security and stuff like that. So for now, what I want to do is I want to go back inside of the heading component. So heading.tsx inside of marketing folder underscore components right here. And let's go ahead and let's add our hook use convex out so we need to import that from convex react import use convex out from convex slash react all right and let's go ahead and let's extract is authenticated and is loading perfect and now let go ahead and let wrap this button inside of if we are not authenticated Sorry if we are authenticated So remove this exclamation point If we are not loading, only then do you render this button to actually enter Joshin, right? Otherwise, we're going to go ahead and check for is loading. and we're going to render the spinner from add slash components spinner, which we created recently. So make sure you have that imported and give it the size of large like this. And now if you refresh, you can see here for a second, you have a little spinner here. And let's go ahead and wrap this inside of a div. And let's give it a class name of w-full flex items-center and justify-center. And now if you try again, there we go. Now it's in the center. Perfect. So exactly as we want. And now what I want to do is do just modify this to actually lead you to the documents page because this is the case if we are authenticated. So mark this button to be as child and inside add a link component from next slash link. So make sure you import that. Make sure you added link from next slash link as I did right here. And give this an href of slash documents like this. And nothing much should change here except now it should lead to slash documents, which if you click, it's a 404 page. It doesn't exist yet. And now let's add if we are not authenticated and if we are not loading. In that case, go ahead and render the sign in button, which you can import from clerk react. So import sign in button from at clerk, clerk react like that. Give it a mode of modal like this and just go ahead and add a little button inside. Get Joshin free. And also add an arrow right, which you already have imported in here with a class name of h-4, w-4 and ml-2. And there we go. So if you try and click on Enter Dotion, we are redirected to a 404 page. But if you go ahead and log out, as you can see now, this button as well opens a GitCup. So exactly what we wanted. And if we log in from anywhere, everything refreshes. And as you can see, now we have both redirects to currently non-existing page. Great, great job. So now that we have our database and our authentication set up, let's go ahead and let's create this page, which currently yields a 404. So let's just recap what we have to have running for this application. So let me remind you of my terminals. So in one terminal, I have npx convex dev running, which is my backend. And in the other one, I have npm run dev running. So make sure you have two of those running, refresh your page, confirm everything is working. Great. So what we have to do now is we have to create an organizational folder, which is going to help our documents page, which is currently a 404. So let's go inside of the app folder. And the same way we created this marketing route group, let's go ahead and let's create a new route group, which we are going to call main. So main application is going to go inside. And let's go ahead and let's create another folder inside called routes. Like that. And then inside of it, create a new folder, documents. And inside our file, page.tsx. And then you can go ahead and create documents page with a div. This is a protected page. So only logged in users should see this page. And once you save that, you should no longer get a 404 error. If you're still getting a 404 error, make sure that you are on slash documents and make sure that all of your links inside of this landing screen lead to slash documents. So now let's try and log out. And let's see if we can manually go to slash documents. And it looks like we still can. So that's not good. If we are logged out, I want to get redirected. So if the user manually goes here, I want to bring them back to the landing page to log in. So in order to do that, we can either use the middleware or we can simply create a layout inside of this main folder. And let's go ahead and create a layout.tsx inside of the main organizational folder. So layout.tsx. So it is inside of the main folder, not inside of the routes. So it's separated like this. And now let's go ahead and let's write SFC documents, sorry, main layout. Let's extract the children. Let's give the children a type of React.react node. And let's go ahead and let's return a div, which is just rendering the children. So nothing should change much for now. But what I want to do now is add some hooks here. So in order to do that, we're going to have to map this entire thing as a used client. And that is okay. So usually I would avoid used clients because I want to leverage server components. But this time we are working with a real-time database which shines in client components. It is not a mistake or an anti-pattern to work with client components inside Next13. Next13 just gives you an option to work with server components if that's what your application needs. But since this is a heavily real-time application, it makes sense that we use a lot of client components. Great. So now that we marked our layout as use client, let's go ahead and let's extract is authenticated and is loading from use convex out. And we can import use convex out from convex slash react like that. Perfect. And now let's check if we're loading. We're going to return a div, which is going to render our spinner component, which we created in the previous parts of the tutorial. So make sure you import that. And let's give it a size of large. And let's give it a class name of h-full flex items-center and justify-center. So it's centered in the middle of the screen. So if you try and click on documents now, I'm actually going to go manually to slash documents. You can see how for a second I have a loading indicator in the middle of my screen. Great. And now we're going to check if we're not authenticated. So if we are not authenticated, so if not is authenticated, in that case, we're going to return a redirect from next slash navigation. So make sure you add this import, redirect from next slash navigation. And we're going to lead the user back to our landing page. And there we go. Now that I saved the file, I'm immediately redirected. So if I go ahead and manually go to slash documents, you can see how it doesn't let me go there. I'm immediately redirected back here. And since we did this logic inside of the layout file and not inside of a specific page, that means that every single route that we create inside of our main application is now officially auth protected. Perfect. So that's exactly what we wanted to do here. So now what I want to do is I want to style this div a little bit. So let's go ahead and give it a class name of h-full flex and let's give it a special background if we are on dark mode. So if we are on dark mode, the background we're going to use is a specific hex color 1f 1f 1f like that. And let's go ahead and let's wrap these children inside of a main element just like that and let's give this main element a class name of flex-1 h-full and overflow-y-auto great and now let's go ahead and in here render navigation component and if you save well you will not see an error unless you log in so let me just sign in with my github now so i can go to that slash documents page and there we go now I have an error here great so let's go ahead now and inside of this main folder go ahead and create another folder underscore components and inside of it create a new file navigation.psx let's go ahead and mark this as use client and let's export const navigation just like that and return a div saying navigation and save the file. Now go back to your main layout and import that navigation from dot slash underscore components slash navigation. And I'm just going to separate this imports and there we go. We no longer have any errors. And you can see how navigation is at this side of the page. And now what we're going to do is we're going to slowly create this navigation page. So let's go inside and let's prepare everything we need. So first thing I want to do is I want to wrap this entire thing in a fragment of its own, like this. And I want to change this to not be a div. Instead, we're going to use an aside element. Great. And now what I want to do is give this a class name. So class name is going to be group slash sidebar h-full bg-secondary. like this overflow-y-auto relative flex w-60 like that is and sorry w-60 flex-call and z-index is going to be 99999 so five nines like this because we're gonna have a Notion style block editor, which takes a lot of Z index space. So we are going to move this to the top. Great. So as you can see now, we have a template for our sidebar right here. You can see how it takes this entire space and everything else is moved to this side. Perfect. Now let's go ahead and let's fill this side element with some placeholder items. So what I want to do now is I want to create a div here and I just want to add a paragraph inside action items like this. So we're going to have action items. Then go ahead below that and create another div and give it a class name of margin top four And inside we going to create a paragraph and we just going to write documents like this so first we going to have some action items then we going to have some documents and then go ahead and create another div right here and this one is going to be a bit different in fact it's going to be a self-closing div like that and give it a class name of opacity dash zero like this. And then what we're going to do is we're going to leverage this group slash sidebar. So in Tailwind, there is a class name called group. Usually you write it just like this. So group, right? And then what you can do is you can do group dash hover opacity dash 100. That means that this element is going to be hidden. In our case, it's going to have opacity of zero. But when we hover on the entire aside element, it's going to change to 100. So why did I write group slash sidebar here? Well, I wrote that because that's a way you can differentiate between different groups. So inside of this sidebar component, we're going to have different groups, which we're going to use that trick on. But we can do group slash sidebar. So we tell it, okay, this is only supposed to be active on group slash sidebar. And if you change group slash sidebar here, you also have to change it here. But it's not going to be group slash sidebar like this. instead it's going to be group dash hover slash sidebar like this and you can see when i hover over this class it's a completely valid class it's working great so now that we have that let's continue adding some classes to this element so we're also going to give it a transition class we're going to give it a cursor dash ew dash resize an absolute h dash full w dash one bg dash primary slash 10. So we're going to give it some opacity, right dash zero and top dash zero. And now if you take a look, when I hover over my sidebar, I have a little bar here, which is going to be used, you guessed it, to expand our sidebar as needed. So let me try and zoom in so you can see. You can see when I hover anywhere on the sidebar, I have this opened to me. So it indicates to the user, okay, there is something I can do with this. And that's exactly what we want. Now I want to go back inside of this aside action right here above this div where we wrote the action items. And let's go ahead and add a new div here, which is going to render Chevron's left icon from Lucid React. It's a self-closing tag. Just make sure you import Chevron's left from lucid react and let's go ahead and let's give this a class name of h-6 and w-6 and let's give this div a role of button and let's give it a class name of h-6 w-6 text-muted-foreground like that, rounded dash small, hover, vg-neutral-300, on dark mode, hover is going to be vg-neutral-600, absolute top dash three right dash two opacity dash zero and we're also going to modify it when we hover so on group dash hover slash sidebar we're going to change the opacity to 100 and we're going to add a transition class great so you can see that now when i hover we have a nice little icon here, but only when we hover. So it's not visible. It's not visible right now, but when I hover, you can see that I have this little icon, which you guessed it is going to be used to collapse the entire sidebar as we need it. Perfect. So now that we have this, I think we're ready to create some refs here. So let's go ahead and let's go here and let's write const is resizing ref to be use ref and give it a false value then const sidebar ref is going to be use ref and it's going to have a type of element ref from react so make sure you import that open another pair of pointy brackets and go ahead and write an element called aside and let's give it a default value of null. Below that, we're going to have a const navbar ref, which is going to be use ref again. It's going to be an element ref with a type of div inside and a default value of null. Great. And now let's also just add const is resetting. Set is resetting to be used state from React and give it a false default value and const is collapsed, set is collapsed to be useState false as well. Great. So now that we have that, I want to go ahead and install a package called useHooks. So let's go inside of our terminal. I'm just going to open a new terminal and I'm going to write npm install usehooks-ts. So a very cool package. If you want to, you can also search for a specific hook, which we're going to use outside of this package and just create that hook. But for a tutorial, it's just simple for me to install the entire package. So import use media query from usehooks-ts. And so the reason I'm not using Tailwind breakpoints for this is because it's quite complex to do that, especially with the fact that our sidebar is gonna be resizable on drag, as you saw in the demo, right? So it's quite hard to do that using just Tailwind. So we're gonna have to use some tricks with refs and other stuff. And we're gonna need use media query to manually define in JavaScript what's considered mobile and what's considered desktop using breakpoints. So I'm gonna go above all of this and I'm gonna write const is mobile to be use media query. And I'm gonna go ahead and open a string and write max dash width is going to be 768 pixels. So this is the same breakpoint for medium inside of Tailwind. And then we can change this is collapsed, use state default value to be is mobile. So if we are on mobile, our sidebar is gonna be automatically collapsed. But if we are on desktop, we're gonna have an option to collapse it once we click on this item right here. And while we are here, let's also go ahead and let's add params here. So const, sorry, path name is going to be use path name from next slash navigation. Because if we are on the mobile, I also want to manually collapse the sidebar every time we click on a different item, right? I don't want our sidebar to stay open because it's going to take the full screen. So I want when user clicks on a specific document to collapse the entire sidebar. And that's why we're going to use this path name inside of a use effect so we know when that happens. So now that we have that, we're ready to start writing some functions here. So let's go ahead and let's first, well, let's define some functions actually. But before we actually can define any functions, we have to assign these refs which we just created. I completely forgot about them. My apologies. So let's go inside of the aside element right here. And let's give it a ref of sidebar ref. Like that. Great. And now that we have this as well, let's change this into curly brackets like this. And let's add a CN library from at slash lib utils. So just make sure you import CN from lib utils. I'm just going to separate this import. And once you have that CN right here, go ahead and wrap this entire class name inside the parentheses like that. And you can press enter like this. Whoops. and you can just press enter like this. So it's in the first line. And then in a second line, so add a comma after this entire string ends and let's add some conditional classes here. So if is resetting, resetting, which we define as a constant here. In that case, we're gonna add a transition to it. So transition-all, ease-in-out, duration-300. And let's also add if is mobile. In that case, width is zero by default. And don't forget to add a comma here. And as you can see now, I no longer have my sidebar here when I refresh. That's because the is mobile right here is true. And we wrote a class theme here. If we are on mobile, the default width of the sidebar is zero. But only you can see even when I expand only then it's visible, right? perfect so what I would recommend you do is just zoom out so you can actually see the sidebar that you're working with great so we just assigned the sidebar right here now let's go ahead to this button here and let's go ahead and do the same thing with the classes so I'm gonna go ahead and open this wrap this in curly brackets add the cn library wrap the entire thing inside of cn like this. And let's go ahead and add conditional one if is mobile. In that case, opacity is going to be 100 by default, right? So if we are on mobile, we're not going to do this trick that only when we hover on something does a button appear. That's okay for desktop. But on mobile, we want it to always be visible so user knows that they can collapse the sidebar because there is no, well, there probably is some kind of hover on mobile, but it's not the same user experience as on desktop. So if it's mobile, we're not going to do this whole on hover, change the opacity. It's just going to be visible all the time. Great. And now that we have this, let's go ahead all the way to the bottom here, outside of this aside element. And let's go ahead and let's create a div here, where we're going to have a ref of navbar ref. And we're going to have a class name to be cn. Default classes are going to be absolute Top dash zero Z index of one, two, three, four, five So five nines like this Left is going to be 60 And width is going to be a calculation Of 100% minus 240 pixels Because that's the value of this left 60 right here So 240 pixels Great And then we're going to add some conditional classes so it is resetting. In that case we going to add a little transition here So transition ease and duration And we also and just make sure you don misspell resetting And we're also going to have another condition, if is mobile. In that case, left is going to be zero and W is going to be full. So what we did here is our position for the navbar, which is going to be right here where it currently says this is a protected endocuments page. So this navbar is going to have to follow the width and the collapsement and everything that the sidebar does, it's going to have to be in sync with the navbar. So that's why we need this a bit of a complex logic here to watch for all of those actions to happen. Great. So now that we have that, let's go ahead inside and let's add a little nav element here like this and let's just write if is collapsed. In that case, add a menu icon from Lucid React. So make sure you import menu icon from Lucid React and go ahead and give it a class name of h-6, w-6 and text-muted-foreground. Let's also give it a roll-off button like this. And you can see that now when I zoom in, I should have, there we go. So you can see it's overlapping the text currently. So it doesn't make much sense. But as you can see right here, there is a little icon here. You can see how it's over my text. Now let's go ahead to this nav element and let's give it a class name of bg-transparent, px-3 like this, py-2 and w-full like that. Great. So that's enough for us for now. And now what I want to do is I can finally go ahead and create some actions. So let's go ahead and let's find this div right here. So this is the div, which appears when we hover on our sidebar, this little bold line right here, that is this div. So let's go ahead and let's modify it a little bit by giving it an on mouse down option. Let's just put that as an empty function and on click option, which is also going to be an empty function. So first, let's resolve this on mouse down option right here. So let's go ahead and let's create that function here. const handle mouse down is going to accept an event, which is a type of react.mouse event, html div element, and mouse event, like that. And it's an arrow function. And the first thing we're going to do is we're going to prevent default. And then we're going to stop propagation from this event. And then we're going to set is resizing ref dot current to be true. And we're going to add an event listener. So document dot add event listener. Whoops. Well, listener. All right. And event listener is going to be mouse move. And it's going to trigger handle mouse move like this. and go ahead and copy the addEventListener. And let's just not misspell this. So addEventListener. Okay, I made a typo here. So addEventListener like this. And this one is going to be not mouseMove, but mouseUp. And the function it's going to call is handleMouseUp like this. So now obviously we're going to get some errors because none of this exists right now. But let's just go ahead and assign this handleMouseDown to this on mouse down empty arrow function, which we created right now. And let me just see if I refresh, if I immediately get an error. I don't, but I think that when I try and move this, it looks like I'm not getting any errors, but I think I am getting in the console. Nevertheless, let's go ahead and let's create this handle mouse move and handle mouse up. So handle mouse move is gonna be used to resize our sidebar. But when we leave the mouse, when we put the finger above our cursor, that means we are done with resizing and we can stop resizing, right? So first let's do the handle mouse move. So let's go below that and write const handle mouse move is gonna accept an event to be a mouse event like that. And first thing you're gonna check if we have the is resizing ref inactive. So if there is no is resizing ref dot current, In that case, we can just break the entire function. No need to go further. And then let's assign the new width to the client X value of this event. Select new width. It's going to be event dot client X like that. Actually, it's not. Let's just rename this to event like that. There we go. And now let's add some limits to how much the user can resize the sidebar to and how much they can collapse the sidebar. So if new width is less than 240, in that case, new width is going to be 240. So we can now go lower than that. And then in the same way, we're going to limit how wide it can go. If new width is larger than 480, then new width is simply going to be 480. We're not going to let them go above that value. And now that we have that, let's check if we have both the sidebar ref and the navbar ref. So if we have sidebar ref dot current, and if we have navbar ref dot current, because we need to animate both of them now, in that case, let's give the sidebar ref dot current dot style dot width, open backticks, add an object inside new width, with pixels like this. And then let's also add the navbar ref.current.style.setProperty, like that. We're going to set the property left to be moved. Let's just open back things again, to new with PX, like that. So let me just expand this a little bit. There we go. Okay, one more time. There we go. So we moved the navbar left property by the same amount. And now we also have to move its width. So navbar ref.current.style.set property width is going to be, open back things again, calculate, open brackets, 100% minus, open the special object, new width, and add pixels inside of these brackets right here. So let me try if I can expand even more so you can see this in one line. So this is how it's supposed to look like. Navbar ref.current.style set property width open backticks and calculate 100% minus the new width to which we just expanded pixels and then end the bracket and end the backtick. Great, so now that we have all of those, we are ready to create this handle mouse up function. You can see we no longer have an error for this one, but we have the handle mouse up error. So let's go below this and let's create a constant handle mouse up, which all it's going to do, it's not even going to have to take any events. All it's going to do is set the is resizing graph dot current to be false and document dot remove event listener mouse move from handle mouse move and document.remove event listener mouse up from handle mouse up like that then if we did everything correctly I think let's just confirm so there are no errors perfect if we did this correctly I think we should be able to move our sidebar now so let me try and expand this now and there we go you can see how I have a limit to how much I can expand and collapse my sidebar and I can leave it like this and you can see how it's no longer following my mouse but if I hold on this it's following my mouse to a specific breakpoint which we've given it and because of all the flex properties we've given inside of our layout you can see how the entire content is moving with it as well and you can see it right now but in the future we're also going to have a nav bar here, which is just as well going to follow the transition of this sidebar. Perfect. So now what we have to do is we have to create this functionality that collapses the entire navigation bar. And we also have to create this, that when you click here, it actually opens the sidebar. Great. So first thing I want to do is this button right here. So let's create the functions to handle that. So I'm just going to expand my entire screen here because we're going to focus on the code now. And let's go below this handle mouse up and let's create a constant which is going to be reset width. So reset width is going to reset the sidebar width to its original width. So it's not going to collapse it. It's going to be used if we expand too much and the user wishes to go back to the original position without dragging. They can just click on that line they used to drag and then it's going to be reset to 240 pixels. So let's go ahead and first let's check if we have the sidebar ref.current and if we have the navbar ref.current. If we have both of those we can continue. So set is collapsed is now going to be false. Set is resetting is going to be false as well. My apologies is going to be true because we want to animate the resetting of the width. You're going to see that in a second. And now let's write sidebar ref dot current dot style dot width. And first, let's check if is mobile. We're going to reset it to 100%, otherwise to 240 pixels. Because in mobile, remember, we take the entire screen with a sidebar. And now let's write navbar ref dot current dot style dot set property. So with Navbar, we work with the property width and with property left. So first, let's assign the width property. Now, let me just do this in two lines. So you don't have to do it in two lines. I just want you to be able to read what I'm writing. So the property is going to check if it's mobile. In that case, it's going to be zero. Otherwise, it's going to be.\na calculation of 100% minus 240 pixels, like this. And below that, let's do the same thing, but for the left property. So navbar ref.current.style.setProperty. The property we're going to modify is left, and the value is also going to be dependent on the isMobile boolean. So if it's mobile, 100%, otherwise 240 pixels. Great, so now we have that. And now let's just write a setTimeout here. We're just going to set isResetting to false. So you might be confused by this action. Why are we doing this? Why this 300 milliseconds here? What is this resetting even used for? Well, if you take a look at our code, when the isResetting is active, we add a little transition to it with a duration of 300. So that's why we use the timeout to be 300 as well. we use it here on the sidebar itself which is the aside element and we also use it for the navbar duration 300 and transition when we are resetting so we are using that because this is not going to be a drag and a hold like when we are expanding this is going to be when we click just reset the entire thing so we want to show just a little nice animation for it and we have to use this timeout trick for it. Great. And if you're wondering, well, why not just, where is it? Why not just use this in the main classes? Why do we even need it in the Boolean? Well, that's because if you add transition all, and if you add this class to the main classes here, it's also going to be visible when you drag and resize. And it just looks very off. It looks weird. And there's no way to limit it to a specific action in Tailwind besides using a combination of JavaScript with it as well. Great. So now that we have the reset width option, let's go ahead and let's assign it to this div. There we go. We have this on click, which is an empty arrow function. And let's now give it the reset width option like this. And now let me expand my screen and let me just refresh. and now we can see when I expand this and then when I click on this oh looks like it's not is it working or not let's just see there we go so it's working as you can see when you expand just go ahead and click on this element and as you can see how it nicely creates a little animation here to collapse back to its original position and it also moves the entire content with it as well in a nice animated way. Perfect. So that's what we wanted to achieve with that reset with function. But now we have to create the collapse function, which is going to be similar to this reset with function. So let's go ahead and let's write const collapse. Let's check if we have the sidebar ref dot current. And if we have navbar ref dot current, and let's set is collapsed to true, and set is resetting to true as well. And then let's do sidebar ref dot current dot style dot width is simply going to be zero, like that. And navbar ref dot current dot set property, sorry dot style dot set property the property width is simply going to be a hundred percent and navvar ref dot current dot style dot set property left is simply going to be zero and set timeout at the end where we just change the set is resetting to false after a 300 millisecond delay. And now we're going to assign this collapse function to the on click of this button right here. Where is it? There we go. This roll button right here, which we've given. Let's give it an on click of collapse like this. So it's this div just inside of the aside, which holds the chevron's left icon. And now let's try that out. So I'm going to refresh again. I'm going to zoom out just a bit. And you can see when I click here, you can see how it entirely collapses. And when we click on this one, well, I think we can already reuse the reset width on that one. And that's going to work. Let me just confirm if that is the case. So let's go ahead now. Yes, we can just do that. So let's go all the way to the bottom and find this menu icon where we have the roll button. And let's give it an on click of reset width like this. And let's try this out now. So I'm going to refresh this. I'm going to try and collapse this. I'm going to go ahead and click this. And there we go. You can see how I can now entirely collapse my sidebar and I can expand it. Perfect. So now what I want to do is I want to write some use effects, which are going to detect whether we are on mobile or not. As you can see now, I switched to mobile mode, but I can still very much visit. And you can see how you're on mobile. When I expand, it takes up the whole screen. But still, what I want to do now is you can see when I expand to desktop, it doesn't really work well, right? When I have a sidebar now and if I switch to mobile mode, I expect this sidebar to disappear, right? To be hidden. Well, we can do that using use effects because we have all the necessary functions here. So let's go to the top of everything here. And let's write use effect from React. so just make sure you import the use effect from react where we have the ref and the state and for the first one we're going to check if is mobile so if we suddenly turn to mobile collapse the entire sidebar otherwise we're going to use reset width like this and the only thing we're going to add inside is is mobile like this uh you don't have to worry about the warning for reset width because we know that it's going to work we know we are working with refs here which are not reactive in a way that use effect expects it so you don't have to add reset width inside this is going to work just fine and below that let's go ahead and add another use effect which is going to work on the path name change so let's go ahead and add if is mobile here collapse again and trigger this on path name and is mobile like this and we don't have the path name so let's go ahead and add it add name perhaps i misspelled it yes path name great and there we go no warnings here now and as you can see now when i expand my screen i have a sidebar when i collapse i don't have a sidebar when i click on mobile it takes up the whole screen But if I go ahead and expand and try then, it doesn't. It does just what we want it to do. If we try from here, it works exactly the same. Perfect. So we have entirely finished our animations of the sidebar. And now we are finally ready to start giving it some items inside. Great, great job. All right. So now that we have our navigation working, what I want to create is this. this is a protected documents page. So currently that's the only thing it says when I'm in this slash documents route. So now I wanna create a little picture here and a little button which is telling the user that they can now create their first note. So in order to do that, first thing I wanna do is prepare the images we're gonna need for that. So I'm gonna go ahead and prepare my public folder like I did for the hero images for the landing page. And let's go ahead and go inside of my GitHub right here. and just go ahead and find the public folder, which I have, and go ahead and download empty and empty dark. So I'm going to go ahead and download this one, and I'm going to go ahead and drag and drop it inside of the public folder. And then I'm going to go ahead and download empty dark, download that as well, and drag and drop it inside of the public folder. Great. So let's see if I have to rename them, there we go. So it looks like I have to rename this empty, it shouldn't have any numbers in it. So I'm just going to rename it to empty.png. So make sure you have empty dark and empty.png like this. Great. So now that we have that, let's head back inside of our app folder, main folder, routes, documents, page.dsx. So here where we say this is a protected document page. And what I want to do is add a class name to this div with h-full, flex, flex-call, items-center, justify-center, and space-y-4. So we now center this text right here. And let's go ahead and let's mark this entire thing as use client because we're going to have some hooks inside. And let's remove this text from here. And instead, let's render an image element from next slash image. Let's go ahead and give it a source of slash empty.png. Let's give it a height of 300, a width of 300 as well, and alt of empty. And now let's give it a class name of dark hidden like that. And now let's copy and paste this just above, just below this one. And let's change this one to be empty dash dark. and let's give this one to be hidden on default. But when we switch to dark mode, we are in block. Perfect. So you can't really see that now because we don't have the functionality to switch light and dark mode inside of this main page. But as you can see, we have an empty little image right here. Perfect. And now below that, I want to render welcome to your name's potion. Sorry, your name's Jocean. That's what we're gonna call the app, right? So let's go ahead and let's import useUser from Clark React. So import useUser from at Clark slash Clark React, like that. And let's go ahead and let's extract the user from useUser like this. And now, just below this image right here, what we're going to do is we're going to add an h2 element where we're going to say welcome to and go ahead and write user? question mark dot first name and then add a special character apples like this so this is a way to escape the actual this is the sign we're trying to write but as you can see I have an error that it can be escaped so that's why I'm writing apples like this so welcome to your names Joshin like that now it says welcome to Antonio Joshian in my case And now let go ahead and give this heading element a class name of text and font Great. And below that, let's add a button element. Make sure you import that from as slash components UI button. and inside of that element, go ahead and add a plus circle icon from Lucid React. So again, make sure you have that imported. Let's give this plus circle a class name of H-4, W-4 and margin right of two. And let's write create a note text like this. Perfect. So we have now finished our empty screen right here and it looks much better than what it previously was. So what I want to work on now is the back in the navigation bar right here. So I'm just going to expand my screen a bit because I want to work in desktop mode for now. And we're going to go ahead and replace this action items with our actual first item, which we're going to use to sign out and to see the currently logged in user of this Joshin, right? So let's go ahead and let's go back inside of our app folder, main components, navigation.csx. So here where we wrote all of that logic on how the navigation bar should behave. And now let's go all the way down and let's find this first div where we wrote a paragraph with action items. And now let's remove that. And instead, let's render the user item component like this. If you save, of course, we're going to get an error because user item is not defined. So inside of that underscore components folder, create a user-item.dsx file. And let's go ahead and let's mark this as use client like that. And let's export const user item, user item, and just return a simple div user item. So now we can go back inside of navigation and import that user item from dot slash user item, because they are in the same folder like this. And there we go. We no longer have an error. And instead, we have a little text here saying user item. So let's go back inside of this user item. And before we continue, let's go ahead and install a couple of chat CN packages, which we're going to need to develop this to the end. So I'm going to go ahead and write npx chat cnui at latest add avatar. So that's the first one we're going to add. So go ahead and click Y if you get this. If not, you're just going to get it installed. And besides the avatar, we also need a drop down menu. So let's just wait a second for this to install. And after this is installed, we run a similar command npx chat cn-ui at latest add drop down dash menu like this. So just go ahead. Looks like it says the drop down menu already exists. Oh, so we already installed that, I believe, when we created the mod toggle button, right? So you can just press no on this one. It doesn't really matter. Great. Looks like we already have this. My apologies. If you don't have it by any chance, feel free to add it using this command. So let me just confirm inside of the code that I actually have the drop down menu. So go inside of your components folder instead of the UI. And there we go. It's right here, dropdown dash menu. Perfect. So we can now work with that. But I think we have a problem here. And the problem is that inside of our navigation, if you remember, we added a Z index of a lot, right? And let's look at our dropdown menu here. I think that in here, our Z index is much lower. It's 50, meaning that it's not going to be rendered in here. So let's go ahead and resolve that. So find the drop-down menu. First, let's find the drop-down menu content. Let's see. Drop-down menu content. So find constant drop-down menu content like this and find where it says Z index 50 and change this to Z index 99999 like this. And besides drop-down menu content, one more thing that we also have to change is subcontent. So let's go ahead and find the subcontent. It's right here. And let's write 9999 like this. So here I wrote four nines and here I wrote five nines. I don't think it really matters, but I'm trying to stay true to my source code. I think I just played around with this until it matched what I needed. All right, so we have the subcontent, we have the content, and let's see if we have any more Z indexes here. I think that should be it. If not, we're going to go back and resolve this. Don't worry if you're having trouble finding this. You can always just copy it directly from my GitHub. We didn't write this anyway. It's just from chat CNUI. Great. So now we can go back inside of the user item right here. And let's go ahead and import everything we need from this newly installed packages. So let's go ahead and let's import avatar and avatar image from at slash components UI slash avatar like this. And let's go ahead and let's import from components UI drop down menu. We need the drop down menu itself. We need a drop down menu content. We need the item like this. We need the label. And we need a separator. Whoops. Where is it? So separator. and we need a trigger. There we go. So we need all of these items from here. And while we're writing imports, let's also import an icon we're going to need, which is Chevron's left, right from Lucid React. Great. So now that we have that, go inside of user item and go ahead and extract user from use user, which if you remember, we can get from clerk so use user not user user but use user from add slash clerk at clerk slash clerk react great so now we have the currently logged in user and now let's replace this tip with a drop down menu now let's write the drop down menu trigger so we have all of this imported Let's give this drop-down menu trigger an as child prop. And now let's write a div with a roll of button. And let's give it a class name of flex items-center, text-small, padding-3, w-full, and hover bg-primary with an opacity, background opacity of 0.5. inside of it open another div with a class name of gap-x-2 flex items-center and the max width of 150 pixels inside of that div it's time to render our avatar which we imported recently and give this avatar a class name of h-5 and w-5 and let's render an avatar image inside with a source of user question mark dot image URL. Great. So now that we have that, let's go below this alatar and render a span element and go ahead and write user question mark full name and write our apostrophe Joshan. So in my case, it's going to be my name, my surname apostrophe S Joshan like that and give this class name, We have this span a class name of text start font-medium and line-clamp-1. So it truncates the line if it's too long. And let's see if we can already see that. And there we go. You can see it right here. You can see how it truncated the lines right here. Perfect. So now we want to add a little chevron here. So it indicates that we can click on that and open our dialog. Great. So let's go ahead now and let's go outside of this drop down menu trigger. Actually, no, sorry, we are still inside of here. So find this div, which encapsulates the avatar and the span and go outside of it and add a chevron's left right icon with a class name of rotate 90 like that. margin left off to text-muted-foreground and h-4 and w-4 and if you revisit your site right now there we go you can see how you have a nice little up and down chevron right here and now it's actually write the content for this drop down menu so let's go ahead and let's write drop down menu content. Let's give it some props. So class name is going to be w80. Let's also give it an align of start, align offset of 11. And let's give it force amount option. Inside of it, let's write a div element, which is going to have a class name of flex, flex dash call space dash y dash four so everything inside is evenly spaced and let's add a padding so we have a bit more room to work with and now we kind of have a paragraph here where we're going to say user question mark email addresses zero dot email address like this and let's write a class name text dash extra small font dash medium leading dash none and text dash muted dash foreground great so now if you take a look and if you click here you're going to see your email address right here perfect so let's go ahead and let's continue and let's write some actions inside so what i want to do is go outside of this paragraph here and open a div and give this div a class name of flex items-center and gap-x-2 and now let's go ahead and write a div class name rounded-mb bg-secondary and heading of one inside let's render an avatar with an avatar image again and let's give it a source of user.imageurl and let's give this avatar a class name of h-8 and W dash eight So let see how that looks now There we go So we have our little image here Perfect And now let go ahead outside of this avatar outside of this div encapsulating the avatar And let's add a new div with a class name of space dash Y. Whoops. Space dash Y dash zero dash one. My apologies. and inside just open a paragraph with the class name of text small and line dash clamp dash one and just render user question mark dot full name appos add an s joshin like this and now it should have a little more options here so we have my email and my name and that this is my joshin right And now we're going to add a little logout button from here. So let's go ahead outside of this div, which is encapsulating the paragraph and outside of this div as well. Actually, we can go outside of all this all the way here at the bottom, but still inside of the drop down menu content and just add a drop down menu separator, which is a self closing tag, and then a drop down menu item. which is going to have a sign out button. Well, first let's add the closing one for this one like this. And then inside, we're going to have a sign out button and we can get the sign out button from the same place we got use user right here from Clark, Clark react. And let's go ahead and oops. And let's go ahead now and let's just render log out and give this drop down menu item a class name w-full cursor-pointer text-muted-foreground and let's also give it an as child option great so now if we take a look one more time when i click on here here's my email here's my name and i can click log out and i am back on the landing page. And let's go ahead and log in with GitHub again. There we go. I can now enter Joshin and there we go. So this is fully working. Great. And now we're finally ready to create our first action, which is going to create our first document. So now that we have some items inside of our sidebar, it's time to enable this button that when we click on create a note, we actually create a new node. So let's prepare a couple of things first. What I want to do before we start anything is install a package called Sonar. So npm install Sonar like this. And this is going to install Sonar. And now you can go ahead inside of your app folder layout.tsx and go ahead and import. So let me just do this at the top. Import poster from Sonar like this. And you can just put it inside of this layout server component inside of the theme provider like this. It's a self-closing tag and give it a position of bottom center like this. You can, of course, play around and modify it to what you like, but this is the one I prefer. Great. So now that we have this, let's go ahead and let's create our schema. But before we do that, just confirm that inside of your terminal, you have let me see this one is this okay, you should have the npx convex dev running, and you should have the npm run dev running. So just make sure you have both of those on. And what we're going to create now is our schema. So let's go ahead inside of the convex folder right here and create a new file called schema.ds like that and go ahead and import define schema and define table from convex slash server and go ahead and import v from convex slash values and now x per default define schema and let's define our table which is going to be documents. So write define table, open an object, and let's give it an option of title, which is a type of string like that, a user ID, which is also a type of string. This is where we're going to pass in the clerk user ID. Below the user ID, add a Boolean is archive. So we got Boolean. we're going to use this for soft deleting of the documents go ahead and add a parent document this is going to be optional so v.optional like this and v.id is going to refer to documents itself so we are going to create a relation with the very same document that we are creating so every document is going to be able to have its own parent document if we create recursive documents inside as you saw in the demonstration, or they can just be all parent documents. Now we're going to have content, which is going to be the actual value written inside of the specific document. And that's going to be an optional v.string like this. And let's use cover image. That's also going to be v.optional, also a type of v.string. Besides cover image, we're going to have an icon, which is again an optional string. And lastly, we're going to have a boolean is published, which is going to be the dot boolean. So published is going to mean visible to any guest, right? So you can share the URL as you saw in the demo. If it's not published, it's just private to you. And now let's create some indexes for faster querying. So index by user. So we can query by the currently logged in user. And we're going to use the field user ID for that. And let's also create an index, a combination of user and a parent document. We're going to use this in our sidebar. So by user underscore parent. And the values which we're going to work with are user ID and parent document. There we go. So we just defined our schema. and now if you want to you can go ahead and visit convex.dev and log in here go inside of your dashboard and just select the project you're working with so notion clone in my case right here and I think you should already have a way to find the schema here if there we go so it says documents right here as you can see and I can click show schema and there we go this is the code that I've just written here and I can see it in my browser. That's really, really cool. So that's how you know that your code from convex right here, thanks to this terminal, which is running. As you can see for a second, I had a wrong schema here, but it doesn't matter. You can just keep npx convex dev running and it's going to watch all the changes you write. And as you can see, it added table indexes and invalidated my schema. And then I have that schema right here in my browser. So I can see exactly this is correct. And the convex server is working perfect. So if you want to keep this open, because now I'm going to show you how this data will appear here. And you can of course, play around and run some functions from here. But we don't have any API functions yet. And that's what we're going to create now. So let's go ahead inside the convex folder and create a new file called documents.ts. And let's go ahead inside. And first, let's go ahead and let's import v from convex slash values. And let's go ahead and let's import mutation and query from dot slash generated server. And let's also import dot and id from dot slash generated slash data model. Great. So now that we have this, we are ready to create our first mutation. So let's write export const create to be a type of mutation. And let's go ahead and give the arguments for this function. So the arguments are going to be a title, which is a v.string. So when we create a new document, we're going to have to pass in the title of that document. and we're also gonna have an optional parent document. So we're going to reuse this function if we are creating a child of another document or it can just be a completely new parent document. So that's why we're gonna write v.optional like this and then v.id documents. Great. Besides this, we're gonna have a handler function and that handler is going to be an asynchronous function which is going to have the context and the arguments like this. And then we can fetch the currently logged in user using context. So const identity is going to be await context.auth.getUserIdentity. And if we don't have the identity, that means we are not logged in. So an unlogged, unauthenticated user is trying to create a new document. We won't allow that. So throw new error, not authenticated. there we go and now let's extract the user id from the identity so const user id is equal to identity dot subject and now let's finally create our document so const document is equal to await context dot database dot insert and we are inserting documents and now let's define exactly what we're inserting. So the title is going to be arguments.title, which we defined right here to be a type of string, but we are missing a couple of stuff. So we need a parent document. If it exists, args.parentDocument. If it doesn't exist, this is just going to be undefined. And that's completely fine because in our schema, we said that parent document is optional. And as you can say, one of the options of that is undefined. So this is completely working and we can reuse it. And we're going to have to pass in the user ID, which is user ID or you can just write a shorthand like this and let's set is archived to be false and is published to be false as well and there we go no more errors and now we can just return the document from here and go ahead and save this and you can always check your terminal to see if there are any mistakes as you can see it checking everything and there we go convex functions are So we successfully wrote this function And now let me just refresh my dashboard here And I think that you should already see your functions here There we go. Documents create. So click on the functions here. And as you can see, this is your code. And you can see how many invocations you have, how many errors it has, and how long it takes to execute that. So that's why this convex is really cool. because it almost breaks the barrier between what you write here and what you write in this little interface they have here. Really, really cool. And it makes it so easier to test it out whether it's working or not. Great, so now that we have this, it's finally time for me to show you how you can actually add this inside a React component, right? And as you can see, we are currently not using this, but it's gonna come in handy later. Great, so make sure you save this, Make sure you confirm that you have that in your functions. And now let's go ahead and let's go inside of the app folder, inside of the main folder, routes, documents, page.tsx right here. So we have this button which says create a node. So first let's check if we have use client. We do. Great. And now what we can do is add our create method. So const create is equal to use mutation. And now let's go ahead and let's import this use mutation. So import useMutation from convex slash react. And while we are here, and it's not userMutation, it's useMutation, like a hook. And while we are here, let's also import our API. And we get our API from at slash convex underscore generated API. There we go. So now that we have that, let's go inside of this createMutation and let's write API. and as you can see, we have type safety here. So .documents.create, exactly what we've written in our documents page. And let me just revisit it right here. So it's exactly this, documents. And if I renamed this file, where is it? My convex. If I renamed this documents file into something else, then the API would change. It would be api.somethingelse.create. And same thing if I rename create. So you don't have to follow this exactly as I'm doing it. You just make sure that, you know, it's the same logic behind it. And now that we have this create, we can directly add that on click here if we want to. But what I want to do is I want to create a little handler so I can do additional actions on it, like fire a toast on successful creation and maybe redirect the user to that newly created note. So let's write const on create like that, which is going to be an arrow function. and now let's write const promise to be create and as you can see, it's expecting an argument and that argument is an object of our arguments and as you can see, parent document is optional but title is required exactly as we've written in the arguments right here and that means we just have to pass in the title and for my code, what I'm gonna do is I'm gonna pass in the title untitled for every new one like this, great and now that I have this promise, What I'm going to do is I'm going to add the toast from the toaster from Sonar. So let's go ahead to the top here and let's import toast from Sonar like that. And let's go ahead and let's write toast dot promise. Let's pass in that promise and let's define the states. So if we are loading, we're going to write creating a new, creating a new note. if we have succeeded so success we're going to say new note created and if we fail if we get an error we're going to write failed to create a new note all right so now that we have this let's go ahead and let's add this on click to this function sorry to this button right here so button now is going to have on click on create. Great. So let's go ahead and expand this screen here. Let's see our data here in the convex. So we currently have nothing in the documents table. Let me refresh this. And when I click create note, you can see how it says new note created and let's revisit. And there we go. We have a new document is archived is false is published is false title is untitled. And we have a matching user ID inside. We also have creation time made for us and we also have the ID. So great, we successfully created our first API route. So now I want to show you how you can actually load this information. Now I show you how to create it in React components, but how do you load it? Well, very simply, let's go ahead and let's revisit our convex right here. So where is it? It's a convex folder. Go inside the documents.es. And just for fun, let's go ahead and let's add a new API endpoint here, export const get. And it's going to be a query. And let's go ahead. And for now, we can just do a handler, I believe, like this. It's going to be an asynchronous function, which is going to have the context. It's not going to have the arguments because we didn't pass it. and let's go ahead and let's do this thing. So we're just gonna go ahead and get the identity to make sure we are logged in. And then what we're going to do is we're just gonna write const documents to be await DB, sorry, context.db.query like this. Sorry, context.db.query documents. And we can just leave it like this for now and return documents. So this is a very simplified function. Of course, we are not actually checking whether the logged in user is querying its own documents, we're just loading all documents. But I just wanted to repeat how we do authentication here. And later, we're going to add the index here. So great. Now that we have this, let's go ahead and let's go inside of our navigation. So inside of main components navigation right here. And let's go all the way to the top here, We're going to remove this later, but just for now, I want to show you. So let's go ahead and write const documents to be use query. And we have to import use query from the same place we imported use mutation. So use query from convex slash react. And let's go ahead and pass in the API, which we get from at slash convex underscore generated API. And let's pass in the API documents. And as you can see, it auto completes the get function. And there are no errors because we don't have to pass in any arguments. And now that we have these documents right here, let's go ahead and let's find the div where we actually wrote the paragraph documents. And instead, let's go ahead and write documents, question mark dot map, because they have to load, right? But we are not going to do loading states now. And let's get the individual document. And let's go ahead and return. let me just see I think I made a mistake here just a second let's just go ahead and let's see what we'll end up with here so let's just add a paragraph c here with a document dot title like this and let's see if this is going to break anything or not all right so returned value is a query oh yes I think I forgot to actually write my write my entire query here so go back to documents.get and we have to write.collect at the end. I forgot to do that. And now right here, there we go. You can see no more errors regarding the query. Instead, it's missing a key, which is document.underscoreID. And now there we go. No more errors. Perfect. So just make sure that in your documents, in this get function, you add.collect at the end. Perfect. And now you can just leave the query as it is. So this is where we defined the query. So const documents use query API the documents get, that is fine. And then in the documents, we just iterate over them. Perfect. And now in your sidebar, there we go. It says Untitled. And look at this, when I click Create a New Note, instantly, I have a new one. So this is a real time database, meaning that this use query function, which we defined in the sidebar, is also watching in real time, all of the changes we have. Perfect. So that's what I wanted to demonstrate you. So now that we have this functionality to create a new node, it's time for us to also create the same button inside of this sidebar. And it's time for us to also render these items in a nicer way. And let me show you how you can clear your entire schema from here. So you can just click right here and click clear table and just confirm. And there we go. Instantly, everything is gone and you can start all over again. Great, great job. All right, so now let's go ahead and let's add this create a note button right here in the sidebar as well. So first thing I'm going to do is go back inside of my convex dashboard, and I'm just going to clear my entire table. So I don't want to have anything in my sidebar like this. So completely empty. Make sure you do that. And now let's go ahead and let's go back inside of our navigation right here. And let's go ahead and let's find our div where we have the user item right here. So find the div which is encapsulating the user item. Go below that and go ahead and write an item element which is not defined. So if you save, you're gonna get an error but I already wanna give it some props. So we're gonna have an on click option which is just gonna be an empty arrow function. and we're also going to have a label which is going to say new page and we're going to have an icon plus circle and plus circle comes from lucid react so just make sure that you import plus circle from lucid react so let me just collapse this props so you can see how this is supposed to look like so item is a self-closing tag which has the on click label and icon right here perfect so go ahead and save that. And now let's go ahead and let's go inside of this underscore components where we have the navigation itself and create a new file item.tsx. And let's go ahead and let's mark this as use client. And let's export const item like this. And let's just return a div saying item. And then we can go back inside of our navigation and we can import that item from dot slash item so let me show you where it is there we go so import item from dot slash item the same way we did with user item great and now we can go ahead and save this and let's just see all right and you should no longer have that error if\nIn the dashboard, instead, you should have a little bit, which now says item right here. And now what we have to do is we have to fix this type errors. So we are sending on click label and icon. So let's go inside of the item and let's create an interface item props. And I'm going to go ahead and add a label, which is going to be a type of string. I'm going to add on click, which is going to be a type of void. and I'm also going to add an icon and I'm gonna make it a type of lucid icon from lucid react and now let's go ahead and let's extract all of those props and let's map them to item props so we have label on click and icon great and now let's go ahead and let's go ahead and render the label here so already in your sidebar this should now say new page because in the navigation as you can see we send the label new page. Great. Let's go back inside of the item now. And let's go ahead and give this div an on click option of on click. Let's give it a roll of button. Let's go ahead and let's give it a style. Adding left 12 pixels. And I know you're wondering why am I using the style prop here? Well, that's because we're going to reuse this item element later in the future to create a list of documents which we have. And if you remember, each document can have a child document, which means that we're going to have to do some math to push it more to the left if it's a child, right? So you're going to see that logic later on. So I just already want to prepare that inside of the style prop rather than class name, because then we're going to have to remove the class name and it's just going to be complicated. So let's go ahead and write an actual class name for the rest of the styles here. So we're going to have a group, min-h is going to be 27 pixels, text is going to be small, padding on top and bottom is going to be one, padding on the right is going to be three, width is going to be full. When we hover on it, the bg is going to be primary slash five, it's going to be a flex box, it's going to have its items centered, and it's going to have text muted foreground and we're also going to bold the font a bit by adding font medium. Great. And now let's go ahead and let's render this icon. In order to render this icon, we have to remap it to capital icon. Sorry, icon with a capital I because now we can use it as a JSX element. So make sure you do this inside of your props. So just remap it to icon with a capital I and then you can just safely render that icon just like this. And that's going to render whatever you pass in. So in this case, we're passing in the plus circle. So this is going to render plus circle inside of here. And now let's go ahead and give this icon a class name of shrink dash zero height of 18 pixels, margin right of two and text dash muted dash foreground. Excellent. And last thing we have to do is wrap this label inside of a span element. So let's go ahead and write class name, fruncate like this. And there we go. So let me just go back to the item. I think that should be it for now. So let me expand my screen now. And there we go. Now we have a new page button right here. You can see how when I hover, it has a nice little effect and it has a cursor button on it. Perfect. So now what we have to do is we have to enable that when we click on this, that a new one is created. So let's go ahead and do that now. So what I want to do is I want to go back inside of my navigation here. And let's go all the way to the top where we add the documents query. And what I want to add is the create function. So const create is use mutation. And let's go ahead and let's import use mutation from convex slash react. And now let's go ahead and pass in the API, which is going to be API.documents.create. Great. Now that we have this create function, let's go ahead and all the way before we return something, so after all of this functions for the navbar, let's go ahead and write const handle create. And inside of here, we're going to do the same thing that we did inside of this main page. So we're going to create a promise. Const promise is going to be create function and we're going to pass in the title of untitled. And then what we can do is add our toast from Sonner. So let's go ahead and import Sonner. So import toast from Sonner. Great. And now let's find that handle create function here. Then we can do toast.promise, pass in that promise, And let's give it a loading state of creating a new note. Let's give it a success state of new note created. And let's give it an error state of failed to create a new note. Like this. And now let's add this handle create in this item right here where we currently have an empty on click function like this. And now let's see what happens. So I'm going to refresh everything. I have nothing inside of my documents here, but when I click here, there we go. A new one is appearing here. So I can do it both from here and from here now. And you can see I have four of them inside of my dashboard as well. Perfect. So now we have our new page button working. And now what I want to create is I want to extend this item component, which we just created and give it a lot more functionality. So it can be reused for both this button, but also for this rendering of individual documents. So I want each document to have its own icon. I want to have different levels of the documents. For example, if this one is a child of this one, it has to be indented a bit. That's what we're going to use the style padding left for. So we're going to double the padding if this one is a child of this one and stuff like that. So let's head back into the item component right here. And first I'm going to do is I'm going to extend the props that we can accept. So I'm going to add the ID prop, which can be optional because there are some items which we're going to reuse like a button for a new page and some items which are going to be actual documents. So they're going to have to have an ID because they're going to be inside of a dot map function. So we're going to have to add their ID, right? So we know where to redirect and stuff like that. And that ID is going to be a type of ID from convex underscore generated data model like this. So make sure you import that right here. I'm just going to separate it. All right. And the way you write ID is open pointy brackets and just write documents. You can see how we have type safety here. Besides an optional ID, we're also going to have an optional document icon, which is a type of string. And we're also going to have an active Boolean prop. we're also going to have expanded we're also going to have is search we're also going to have a level which is a number and we're also going to have on expand so all of these are optional because as you just saw we just wrote when we created the first item we just created the basics it's going to need but all of this else is optional but we are going to use them to render the list of items Great. So now that we have that, let's also go ahead and add all of those things here. So we have an ID. We have a label. Let's go ahead and go below the icon and let's add the active prop. Let's go ahead and add the document icon. Let's add the is search. Let's add the level. And let's give it a default property of zero like this. So even if we don't pass it, the default level is going to be zero. Let's give it an on expand. and expand it. Great. So now that we have all of those, let's go ahead now. And first thing I want to do is add the constant for Chevron icon. So const Chevron icon is going to be expanded, question mark, sorry, expanded, question mark, Chevron down from Lucid React. Otherwise, chevron right from lucid react so just make sure you have chevron down chevron right and lucid icon from lucid react package great now that we have the chevron icon we're going to use it to render that next to individual items so it indicates the user whether they have expanded this item to see its children or not perfect now let's go back inside of this div right here and let's change this padding left to be something more dynamic here. So as I said, we're going to use the level here. So if we have a level, in that case, what we are going to do, so let's leave this as an else clause and go inside of here and open parentheses. Sorry, don't open parentheses, open backticks like this. And inside, we're going to render a special object. We're going to do level times 12, like that plus 12 pixels like this. So padding left is going to check if we have a level. And in that case, it's going to multiply that level by 12. And it's going to add 12 additional pixels, which are these initial 12, which we have right here. If we doesn't have a level, it's simply going to have a padding of 12 as we had in the beginning. So depending on the level that each item is rendered it, meaning how deep of a Nestle child it is of a specific document, that's how much we're going to push it to the left, right? Perfect. So now that we have that, let's go ahead and let's wrap this class name inside of curly brackets because we're going to make this dynamic as well. And to do that, we're going to use the cnutil. So make sure you import cn like I did right here and just open parentheses and wrap the entire string inside of it. Great. And then you can just press enter like this. So it's collapsed. You can clearly see, okay, these are my default classes. And now let's add conditional ones, which is going to be our active prop. So if we are active, we're going to do bg-primary slash five and text-primary. Great. And now let's go ahead inside of here and let's check if we have the ID. So if we have the ID for that, I'm going to use this double exclamation points. And this is going to turn this into a Boolean. So it's going to be true or false. You can see when I hover it says that ID can be either the type of ID documents or undefined But since I going to do a ternary check with it I have to turn it inside of a Boolean So let go ahead and do it like this And in that case we going to render a div which is simply going to render the chevron icon, which is this constant, which we defined right here. So chevron icon, let's go ahead and just not misspell it like this. So it's this conditional one, which is going to show whether we expanded or not on this document. And let's go ahead and give this div a role of button. And let's give it a class name of h-full, rounded-small, hover, bg-neutral-300, dark, bg-neutral-600, and margin right of one. Great. And let's give it an on click option of handle expand. Actually, let's go ahead and let's just give it an empty arrow function. We don't have that handle expand yet. And inside of this Chevron icon, let's go ahead and give it the class name of H-4, W-4, shrink, shrink-0, and text-nuted-foreground-50. Great. And now that we have this, let's go ahead below that and let's add a conditional icon. So right now we always render the icon which we send in the icon prop, right? But we're going to make this a bit different. So we're going to go ahead and open. If we have a document icon, in that case, we're going to go ahead and just render a div with that document icon. And if you're wondering why can we render the document icon like this, but we have to do the whole renaming of the icon right here. Well, that's because this icon is a type of lucid icon, as you can see. But document icon is a type of string because it's just going to be an emoji, right? So that's why we can just simply render the document icon like this. So if we have the document icon, we're going to render that emoji, which the user assigned for its document. Otherwise, we're just going to render a plain old icon, which we initially assigned. Great. And let's go ahead and give this div a class name of shrink-0, margin-right-2, and text of 18 pixels like this. Great. And now what I want to do is I want to go below this span right here. And I'm going to check if this type of item is used for searching items. So is search like that. And inside, we're going to do a KBD element. And we're just going to render a span element right here. And let's go ahead. And let's just render span. like sorry this command item like this and k unfortunately i don't know what is the shortcut for this little uh character if you want it you can just copy it from my source code or if you want to you can just write control or command like this so it doesn't really matter right this is just you know how it's gonna look like there is no functionality behind this and let's go ahead and let's just give this span a class name of text dash extra small and let's give this kbd a class name which is going to be a bit longer so ml auto pointer dash events dash none inline dash flex height of five select dash none items dash center gap dash one rounded border bg dash muted like this. We're also going to have padding on both sides from left and right to be 1.5. Font is going to be mono. Text is going to be 10 pixels. Font is going to be medium. And text is going to be muted dash foreground and opacity. Well, opacity is going to be 100%, but I don't think we actually need this. Great. And now that we have that, we're going to leave it at here for now. There's one more thing that we have to create here, but we're going to come to that a bit later. And now that we've wrote all of that code, you're probably going to notice that, well, our new page has not changed much, right? But let's see what else we can do now with all of these new props which we created. So I'm going to go ahead back inside of my navigation right here, and I'm just going to expand this so I can still see my sidebar. I'm going to find this item where I wrote new page, and I'm going to go ahead and write an item again. But this time I'm going to give it a label of search. I'm going to give it an icon of search from Lucid React. So make sure you import that from Lucid React right here. I'm going to give it a prop is search. I'm going to give it an on click, which for now is just going to be an empty arrow function. And as you can see now, let me just zoom in. You can see how, because I wrote that is a search thing, we now have a nice little shortcut. So the user knows in the future, we don't have that yet. But in the future, user will either be able to click here to search through all of the documents, or they'll be able to use a shortcut command key. Or if you're in Windows, it's going to be control key, right? So I just want to make this capitalized right here. I think it's going to look a bit better if I do that. So let me just go back inside my item and let me find the K right here. I'm just going to make it capital K because I just think it looks a little bit better. There we go. So you can see how now I have this search right here and I have a little shortcut for it here. Currently, it has no functionality. Perfect. So now what we're going to do is we're going to finally render this list of items here. But before we do that, one thing that I'm going to do is just add one more function between these actions, which is still going to be a dummy function. So I go back and set it in navigation and go ahead and copy this search and rename this to settings. Remove the is search prop and give this an icon of settings. Also from Lucid React. So let me just see everything we need from Lucid React is a Chevron's left menu icon plus circle search and settings. Great. So now that we have that, you should see, you should be seeing a new item here. So search settings in new page. The only one that's functional is the new page, right? And now we're finally ready to create our new component, which is going to be called document list, which is going to be a recursive component, which is going to call itself every time you expand on a user. So it's going to be a very complex function. You're going to learn how to write recursive documents, recursive react components in this one. And we're going to leverage our schema, which has the parent document. It's going to be very interesting to see how that's going to work. Great job so far. So now let's go ahead and let's create a new API endpoint, which is actually going to be used for recursive calling for this sidebar right here. So first, I have an error in navigation. Let me just see what that is. Oh, I have a trailing white space. So let me just fix all of those. Trunk is helping us with code quality here. Great. And now let's go ahead and let's go inside of convex. Let's go inside of documents right here. And I'm going to go ahead and remove this get from here. Instead, we're going to write a new one. So export const get sidebar is going to be a query. It's going to have arguments of parent document, which is going to be an optional value. and if it is passed, it's going to have to be an ID of documents. Besides arguments, we're also going to have a handler, which is an asynchronous function, which takes in the context and the arguments which we passed above. And now let's go ahead and let's get our logged in identity. So const identity is equal to await context.auth.getUserIdentity. if we don't have the identity, we cannot fetch these documents. So let's throw new error, not authenticated. And I just look, it shouldn't be normalized error, it should just be throw new error like this. Great. And now let's extract the user ID. So const user ID is identity, identity dot subject. And now let's go ahead and let's fetch the documents. But we're going to do that using this indexes. We have the index by user and by parent. So we have faster querying here. So let's go ahead and write const documents to be await context.database.query documents dot with index. The index we're going to use is by user parent. Let's go ahead and get the query function here. And now you can write all of this in one line, but I just collapsed it so you can see. So I did not open anything here. I just added a new line here. And let's write q dot equals user ID should be the currently logged in user ID. And it also equals the parent document to be equal arguments dot parent document, which if not passed is just going to be undefined. and now let's also add a filter here. Let's also get the queue for query and again, I'm just gonna press a space here and let's write query equals query.field is archived is going to be false. So we don't want to show any of the deleted documents or in our case, archived. And let's order all of those documents by descending and lastly, let's collect them like that. And now we can just return documents. There we go. And I have a trailing white space here. So let me just remove that space. And there we go. Perfect. We have our get sidebar function now. And now let's go back inside of the navigation component. So it's located inside of the app folder, main underscore components navigation. And from here, we can now remove the use query from the convex react. We don't need it. And that means we can also remove this documents use query. We no longer need that as well. And we can also remove this as well. We don't need it. So now that I removed this document list, I just want to show you a little trick. So if you're using trunk, right? At the beginning of the tutorial, we installed an extension called trunk right here. So as you can see because of it we have this improved code quality where it says that I have trailing white space here And usually I fix that by deleting it manually myself But if you want to you can also go ahead and press command shift P or control shift P and go ahead and write settings and select the UI option write this and you can search for format on save. And you can enable this. And then when you save, there we go, you can see how the entire thing is formatted, right? But I'm not going to use that for the tutorial because it changes just too much of the files. So every time that I do something like this, you're going to see me do it manually. So let me just go ahead and find where I rendered this. There we go, right here. So I have the trailing white space. I'm just going to remove it. Great. All right. And now inside of here, let's go and save this and let's go inside of the documents file, sorry, inside of the item file. So item right here, and go ahead all the way to the bottom. And let's add a skeleton for this one. So item dot skeleton is going to be a function, item skeleton is going to accept a prop level, which is going to be an optional number. And let's just not call this item skeleton like this, right? And inside just return a div, like this, and go ahead and give it a style. Padding left, if it has a level, open back text, and right level times 12, plus 25 pixels, because this is going to be a different element. So we have to modify just a little bit. Otherwise, it's just going to be plain old 12 pixels. All right, and inside of it, we're going to render some skeletons. So first, let's go ahead inside of our terminal. let me open a new one and just write npx chat cn dash ui at latest add skeleton like this so just add a skeleton component great and now inside we can go ahead and render the skeleton component from add slash components ui skeleton so and it's going to be a self-closing tag right so just self-close it and make sure you import skeleton from add slash components ui skeleton great and go ahead and write a class name here, h-4w-4, and go ahead and copy it, and write this one to be 30%. I like that. And go ahead and give this a class name of flex gap-x-2, padding bottom and top of three pixels. Great. So now we have the skeleton. Now let's go back inside of the navigation right here and let's render the document list component, which we currently don't have, but we're going to have it in just a moment. So great. Save this file and you should get an error. Now go inside of the underscore components and create a new file document-list.tsx. Perfect. Go ahead and mark this as use client. And now what we're going to do is we're simply going to export document list, export const, document list, return div document list like this. Go back inside of the navigation and you can just import the document list from a dot slash underscore document list. Great. And now that you have this, let's go ahead and let's create an interface. Document list props, parent document ID. is going to be an optional ID from add slash convex underscore generated data model, a type of documents like that. Besides the parent document ID, we're going to have a level, because remember, this is going to be a recursive function. And we're going to have data, which is going to be a type of the document schema. So for that, we can use the doc from a very same import right here. And the doc is going to be a type of documents as well. And it's going to be an array of those. Great. So let's go ahead and extract this props here. So parent document ID and level, which by default is going to be zero. And we don't need to render data here. You're going to see why in a second. All right. And let's just assign this props. So document list props like that. And now let's go ahead and let's get our params from use params from next slash navigation. So just make sure you add this import all right besides the params we're also gonna have router so use the router from next slash navigation all right we have that now and now let's go ahead and let's write const expanded set expanded to be use state and give it an empty front and the object inside and let's import useState from React. I'm just going to move that to the top as well. And we're going to give a type of this useState to be a record, which takes in the string and a boolean. Like that. Perfect. And now let's write a function on expand. So const on expand. It's going to take the current document ID, which we are trying to expand. And all it's going to do is set expanded, prep expanded add all the previous expanded documents and find the individual document ID which we are trying to expand and just toggle the function. So the opposite of the current state of document ID inside of the extended function. Great. And now let's go ahead and let's add a query for our get sidebar with API which we just created. So const documents is going to be use query. Let's go ahead and let's import use query. So import use query from convex slash react. All right. And inside, let's render the API. And I imported API from convex generated API. And let's write API.documents.get sidebar like this. And let's go ahead and pass in some props. So we're going to pass in the parent document to the parent document ID from our props right here. Great. And now let's go ahead and let's write the on redirect function. So const on redirect is going to take in the document ID, which is a type of string, and router.push slash documents, the individual document ID. We currently don't have this router. We're going to have it in the future. Perfect. So now let's go ahead and let's render the loading function. So when using convex, it will never be undefined unless it's loading. So we're going to do a check for that. So if documents is undefined, that can only mean that it's loading. If it's truly failed or wasn't able to find anything, it's going to be null, right? But you can use the undefined state to activate the loading state. So let's write return. let's open a fragment and let's go ahead and import the item component from dot slash item so make sure you add the item like this and since we just added this little skeleton to it we can now use that as our loading state so item dot skeleton like this and let's give it a level of level like that and then let's go ahead and let's write if level is equal to zero in that case let's go ahead and render another fragment here with two item skeletons so let me just close this fragment like this and you can just go ahead and copy and paste this two times like that perfect so now we have our loading state here and now we're finally ready to render this list so change this to be a fragment and let's write the first paragraph here, which is going to write no pages inside. So this is going to be, if we click expand in a document which has no children. So go ahead and give this a style. Adding left, if we have a level, in that case, open backdix and write level times 12 plus 25 pixels or undefined like that. and we're also going to have a class name here. So class name is going to be cn from add slash lib utils. So make sure you add that as well. So cn, go ahead and write hidden text small font-medium text-muted-foreground slash 80. If it's expanded, in that case, go ahead and give it a class of last block. Otherwise, if level is zero, go ahead and render hidden. All right. So this is going to use CSS to detect if this is the only element inside of this entire fragment. That means that there are no other documents rendered and we can safely display it, right? But if it's not last, in that case, it's going to be hidden by default. All right. And now when we are below that, let's go ahead and render our documents. So documents.map, get the individual document. And let's go ahead and let's write a div with a key of document.underscore, whoops, document.underscore ID. And inside, let's render the item element. And let's go ahead and let's give this item an ID of document.underscore ID. Let's give it an onClick function to be an arrow function, which calls onRedirect function and calls the document.id inside so it knows what to redirect to. A label is going to be document.title. Icon, by default, is going to be file icon from Lucid React. So make sure you import file icon from Lucid React. All right. And now let's go ahead and let's edit a document icon of document.icon. Let's give it an active prop of params.document.id to be identical of document.id. Level is going to be level. On expand is going to be an arrow function which calls on expand document.id. Expanded is going to be expanded document underscore ID. So we're going to check inside of that object. And now we're going to do the recursive trick. So in Siege, every item can have children on its own. Guess what component we're going to use for that? Well this component that we are writing right now we going to reuse the entire thing So what we can do is call this component document list It doesn matter that we are writing inside of it we can call it inside, but we're gonna conditionally call it so we don't fall into an infinite loop. So let's write if this element is expanded. So if expanded object, which we hold in our state, holds the document ID and the value is true, in that case, we're going to re-render the entire document list, which we are just now creating. And let's give it a parent document of document underscore ID. And let's give it a level of level plus one like this. Perfect. So now that we have that, let's go ahead and let's check if that is enough for us to actually see something. And there we go. Look at all of my items here. Perfect. Right now, the expand function is not working as you can clearly see. And when I click on any of these documents, I'll lead to a 404 because we don't have that page yet. But if you look, create a new note, you can see how it's immediately right here at the bottom. Perfect. So that's exactly what we wanted. What I want to do now is an option to delete the document, to create a new document, and to actually expand the documents when we click here instead of redirect to them. So let's go ahead and quickly fix this on expand thing. So go back inside the item.tsx right here. And let's go ahead and write a function here. So I'm going to do it above this Chevron icon. Const handle expand. And it's going to take an event, which is a type of react.mouse event, which comes from HTML div element and has a mouse event inside. It's an arrow function. And all it's going to do is prevent the propagation from it. And then it's going to code on expand. But since it's optional, we're going to add a question mark dot and then execute the function. And then add this handle expand. And let's see where we have to assign it. Let me just find it. There we go. It's right here. So find this ID ternary right here and this Chevron icon. And we have this on click right here and just add it here. There we go. And let's see how that looks now. So I'm going to refresh. And now when I click here, you can see how it says no pages inside. Isn't this really, really cool? Because none of these items have any children inside of it. But now we're going to create a little plus icon here, which is going to allow us to create a new document inside. Let's head back inside of our item component. and let's go all the way to the bottom where we render this search thing and go below that and go ahead and add another check if we have an ID. And if we have an ID, go ahead and render a div, which is going to hold two actions, one to add a child inside of that document and the other is gonna be a dropdown where we can delete the document. So write ml-auto flex items-center and gap dash x dash two. And now inside, let's go ahead and let's create another div, which is going to render the plus icon from lucid react. So make sure you add this plus from lucid react here. And go ahead and give this plus a class name of h dash four, w dash four and text dash muted dash foreground and give this div a class name of opacity-0 group-hover opacity 100 h-full ml-auto rounded-small on hover bg is going to be neutral-300 but on dark hover bg is going to be neutral-600 So just make sure that when we do this group hover, we actually have a group inside of our main div and we do right here. So when we hover on the item, only then are these additional actions going to show. So let's see that now. You can see no plus icon now, but when I hover, you can see I have a little plus icon here. And it's only visible on the one I'm currently looking at. Perfect. So now let's actually create an action that when we click on this, we don't get redirected, but instead a child document is created. So let's go ahead and let's create that function. So I'm going to go ahead to the top here, just below the handle expand, and let's write const on create, which is again, I'm going to take an event which has react.mouse event, HTML div element and mouse event. And let's go ahead and check if we don't have an ID, we can just break the function. Otherwise, let's go ahead and let's create a mutation. So in order to create the mutation, the first thing we have to do is add it right here to the top. So let's write const create to be use mutation. And let's go ahead and import use mutation. from convex react. So import use mutation from convex react. And let's go ahead and add our API from convex generated. There we go. So API from convex generated API. Make sure you have that. And then API.documents.create like this. Perfect. And now we can go ahead and continue with our onCreate function here at the bottom. So let's go ahead and add a promise. Const promise is going to be the create function. The title is going to be, as always, untitled. But this time, we're going to pass in the parent document to be the ID of the current item which we are doing this in. So we want to create a child with the parent to be this ID. Perfect. And now that we have that, let's write then let's get the individual document ID. If we are not expanded, in that case, call the on expand optional function like this and router.push. We don't have the router, so let's just quickly add the router here. Const router, useRouter. From next slash navigation, Make sure you import user router from here. All right. And now let's go ahead and just write router.push documents document ID. Like that. Perfect. And now let's go ahead and let's add our toast from the Sonar package. So I'm going to go here and import toast from Sonar. and now we can do toast.promise, passing the promise which we just created a couple of lines above here and as always, let's give it some states. So loading state is going to be creating a new node. Success state is going to be new node created and error state is going to be failed to create a new note. And now that we have this on create right here, let's go ahead and let's assign it to this new div, which should have the roll button and on click to be on create like this. And one thing that I'm just going to quickly do, I'm gonna go back to this onCreate function. And oh, I forgot to stop propagation here. So let's just do event.stoppropagation while we're here. And let's also just comment out this router.push because all it's gonna do right now is show a 404 page, right? So let's just hide that. And now let's go ahead and try this out. So I'm gonna refresh everything. I'm gonna go ahead and click on plus here. And look at this. I can do this infinitely. We successfully have children and you can close them. You can open it and you can see how it dynamically loads them inside. And you can expand the screen right here. And you can see how now you have more space to work with. You can create as many of these nested documents as you want. Amazing, amazing job. And you can collapse each and every one of them. You can try and play around. Just don't spam too much. These are recursive functions and you don't want to create too much bandwidth on your Convex account. Great, amazing, amazing job. And now that we have this plus icon, it's time for us to actually create additional actions to delete the entire document. And then we're going to create a little trash can where we can restore all the archived elements. so now let's go ahead and let's create additional actions next to this plus icon which is going to be used for soft deleting or archiving a specific document so the first thing I want to do is I want to go back inside of our convex folder inside of the documents.ds right here and inside we're going to go ahead and we're going to add a new API endpoint here called archive so let's go ahead and write export const archive, which is going to be not a query, my apologies, is going to be a mutation, which is going to accept arguments of ID, which is a type of v.id documents. Besides arguments is also going to have a handler, which has the, which is an asynchronous function after all. And it holds the context and the arguments which we passed above. So first things first, let's go ahead and let's get the identity. We can just copy that from this right here and let's also immediately extract the user ID. So I'll just copy and paste that here so we save some time. And now let's go ahead and let's attempt to fetch the document using this ID. So let's write const existing document to be await context.database.get arguments.id. And if there is no existing document In that case, we can just go ahead and throw new error, which is going to say not found. And then let's go ahead and check if the user ID of the existing document. So this user ID, which we have in our schema is matching the currently logged in user ID, because if it's not, we cannot modify this document, right? So let's go ahead and write if existing document dot user ID is not identical to the current user ID, which we just extracted.\nfrom the identity subject, let's go ahead and throw new error, unauthorized. So it's not that we are not authenticated, it's that we are unauthorized to modify that document. And now that we have that, let's go ahead and let's change the document. So const document await context.database.patch args.id. So that's the one we want to patch. And the other thing we're going to change is, is archive to be true? But there is one more thing that we need here. So for now, we can just return the document. But what we actually need to happen is that when we archive a specific document, we also want to check for all of its children, right? If I archive the parent, I expect that all the children are archived as well. because, well, you can decide the logic for yourself, but this is what Notion does. So I'm just trying to stay true to the original and it kind of makes sense because this one should not exist without the parent itself. And you might be thinking, well, it's inconvenient that my children get deleted as well, but think of it in another way. What if you want to get rid of all these things, right? It will be annoying that you have to go individually and delete everyone. It will be nice that we can just delete the parent and then everyone else will get deleted as well. So that's what we're going to use that logic for it. So besides editing this document by ID, we need to create a recursive function, which is going to go ahead and try and fetch the parent of each document and see and recursively delete all the children of this document. So let's go ahead and let's write const recursive archive inside of this function, just above this patch function, which is going to be an asynchronous function. And it's going to accept document ID, which is a type of ID documents. And you can get this ID from our dot slash underscore generated data model right here. Perfect. So now that we have that, let's go ahead inside and let's go ahead and fetch the children. So const children is await context dot database. dot query documents dot width index. So we're going to use index for faster search here by user and by parent. Let's go ahead and let's get the query and let's go ahead and write query dot equals user ID to be user ID and equals parent document to be document ID, which we pass in the recursive function. And lastly, let's just go ahead and let's write dot collect like that. Perfect. And now that we have that, we have to create a for loop, which is going to iterate over all of that children and repeat the same function. You might be wondering, well, why are we not using a for each or map? Well, that's because we're going to do a promise inside of that. So if you're familiar with dot map and for each, you know that they cannot do promises inside of them. You cannot do async await inside of a for each or map. For that, you have to use the good old for loop. So let's write it manually. For constant child of children, we're going to do await context.database.patch child ID is archived true like that. So we're going to get all the children from this document and we're going to archive them as well. Perfect. And let me just fix the, which errors do I have here? It says trailing white space, but we're going to continue developing right now just to see if it's something else. Great. So after this, we're going to go ahead and rerun the entire function again. So await recursive archive child underscore ID. So we create a loop here. There we go. So no more errors here. So let's take a look at what we do again. So in our archive function, what we do is we pass the argument ID. So this is the ID of the document we want to delete, or in our case, archive. We check the identity and we attempt to fetch that parent document, which we are trying to delete. If it doesn't exist, we throw a not found. If the matching user ID is not true, in that case, we are unauthorized to modify this. Now let's jump over this quickly. And then what we do is we archive that document. So we use the patch function followed by the ID we are trying to modify. And we modify its data inside of this object by setting isArchived to be true. And now what we do, well, we don't do it yet, but we're going to do it right now. Before we return the document, we're going to call recursiveArchive. And we're going to pass in the args.id. So this very same argument that ID we patched. So after we modify the main document to be deleted, we pass that ID instead of a recursive function, which let's see what does. It attempts to fetch all children which have that ID as their parent document. And then we run a for loop over all of those children and we change them to be isArchiveToTrue as well. But we are not done here, right? Because we have to check every single child one more time to confirm that they don't have any children as well. So that's why we have to repeat this recursive archive inside of itself to go to the end of the nested child. And that way we know that when we archive this document because of this function, everything else will be archived as well. Perfect. So that's what we want to do with our archive function. We can leave it like this for now. And now we are ready to create our UI for archiving. For that, we have to head back into our item component. So go inside of app main components item.tsx right here. And I'm just going to leave this uncommented for now. We will notice it later. And the first thing I want to do is go right here where we have our own create function. As you can see, we created a div here, which looks like it's supposed to hold more elements because we add a flex box and we add some item center and the gap between elements. But right now we only hold one element inside. That's because we prepared to hold multiple elements inside. So let's go inside of it. And instead of a div, let's add a drop down menu from add slash components UI. Whoops, not the separator, but the drop down menu itself. So let's import that from add slash components UI drop down menu like this. And let's go ahead and let's immediately import everything we need from the drop down menu. So I started some imports here. Just make sure you don't accidentally import them from Radix because you're going to think that you did something wrong with the code when it's just an import. So you should not have Radix imported anywhere inside of your code except in the UI folder. but you don't write those components anyway. These are all from Shatsy and UI. Great, so we're gonna have to need drop-down menu. We're gonna have to need the trigger. So let me just find, where is the trigger here? Drop-down menu trigger. We're gonna need drop-down menu content. So let's get that. We're gonna need the individual item. So drop-down menu item. and I think that's gonna be it, yes. So only this are the ones we need and now we can go back to that div where we started with the dropdown menu and let's add a dropdown menu trigger now which we have imported and let's go ahead and let's give it a prop as child and let's also go ahead and give it an on click option where I simply want to get the event and I want to say event.stoppropagation. so it doesn't redirect us right and let's go ahead and let's just indent this let's fix the trailing white space and now let's go ahead and let's open a div here and let's give it a roll of button and let's go ahead and let's render more horizontal from lucid react inside of it so let me just show you where i added that right here more horizontal from lucid react perfect and Now let's go ahead and give this some styles. So class name is going to be opacity-0. But when we do a group hover, we're going to have opacity to be 100. H is going to be full. ML is going to be auto. Rounded, small. Hover, bg-neutral-300. On dark, hover is going to be bg-neutral-600. And let's give this more horizontal, a class name. of H-4, W-4, and text-muted-foreground. And I think that already we should start seeing something. There we go. You can see how we now have this little options button. And when you click on it, it should not redirect you to any page. But if you click directly on this, it should redirect you to 404. But because we stopped propagation on the drop-down menu trigger, it doesn't do that, right? And now what we have to do is write the content so this actually opens something. So let's go ahead and continue developing that. So outside of the drop-down menu trigger, let's add drop-down menu content. We already have that imported. And let's go ahead and give it a class name of W-60. Let's give it an align option of start, a side of right, and force mount. Now let's go ahead and let's go inside and let's write drop down menu item. Let's give it an on click for now to just be an empty arrow function. And inside, let's write a trash icon from Lucid React. So make sure you have the trash imported from Lucid React like that. Give this trash icon a class name of H-4, W-4 and margin right off too. and let's go ahead and write delete like this. Below the drop down menu item, let's go ahead and let's add drop down menu separator, which is gonna be a self-closing tag like this and let's write div, whoops, and let's write last edited by and in here we gonna get the user which we don have yet so let go ahead and do that Let go ahead and go to the top here and just add const user from use user And we can import use user from clerk. So let's import use user from at clerk, clerk react. now that we have our user we can go back inside of this drop down menu content and let's write last added by user full name like that great and let's give this div a class name of text extra small text muted dash foreground and padding too like that and let's see if we did any mistakes if this is going to work or not so I'm going to refresh everything and I'm going to go ahead and click here. And there we go. I now have my little delete button here. And it says last edited by my name right here. Perfect. So what we're going to do now is we're going to create the button here. When you click on this, it actually archives the entire thing. And since we have the API ready for it, all we actually have to do is just add a mutation for that. So let's go ahead to the top here. We already have the create mutation. So below that, add const archive we use mutation api.documents.archive and now let's go ahead and let's add const on archive which is going to take an event of react.mouse event html, whoops html div element and mouse event and inside we're going to go ahead and write if there is no id just return the entire thing i mean break the function and then const promise is going to be archive and pass in the id just like that and now let's go ahead and call our toast so toast.promise is going to get that promise on the loading is going to say moving to trash. Success is going to say note moved to trash. And error is going to say fail to archive note. Great. And now that we have this on archive, well, one thing I forgot to do was stop propagation with the event. So let's first do that event stop propagation here at the top. And now let's copy this on archive. And let's finally assign it to this empty on click with the drop down menu icon, which holds the trash icon and the delete right here on archive. Great. And let's try this out. So what I'm actually going to do first is I'm going to go back inside of my convex dashboard. I'm just going to clear the entire table. So it's easier to look at what we actually have right here. And it looks like we have this little bug here. We're going to revisit that. But for now, I'm not going to focus on that now. But let's create a new page here. and let's go ahead and see here. As you can see, it says is archived false. Let's go ahead and let's try and delete it. And there we go. It says no to move the trash. And let's see right here, is archived is now true. Perfect. And let's see what happens if I create a new page and inside of it, I create a couple of children like this. As you can see, now I have a bunch of these new documents, which none of them are archived. And if our recursive function is working correctly, when I deleted the parent, all of the other children should get archived as well. Let's see. And there we go. All of them are archived. Perfect. And if you're wondering how come they are not rendered just because they are archived, well, let's revisit our API right here where we have the get sidebar function. As you can see, we have a filter here which ensures that it only shows those that have is archived on false, which means that all of this here, which have it untrue, are not going to be shown here. So the first thing we have to do next is just fix this little issue where it says no pages inside here. I don't know why that's here. And then we're gonna have to create a little trash box here so we can safely recover our files if needed. Great, great job. So let's go ahead and let's try and resolve this little issue that we have here. So basically when I have no documents that should be rendered here, this shouldn't be rendered as well. So let's go ahead and first thing that I want to do is revisit our navigation component. So let's go inside the app main components navigation right here and let's go ahead and let's find where we render this documents list. So we render it right here. Let's just go below it and let's add another item here and let's go ahead and let's give it and on click to be on create, which we already have, or is it handle create? Do we have it? It's called handle create. All right. So handle create, let's go ahead and let's give it an icon of plus from Lucid React and a label of add a page. So we're going to have two ways to add a page here. And I just added this plus icon from Lucid React. So make sure you have that. Let's see. Okay, it looks like it was that. Great. So I just need another item on the bottom here. Perfect. So yes, just like in Notion, we have two ways to add a page, either from here or from here, right? And let's see if now when I delete this and when I delete this, there we go. We no longer now have that no pages inside issue because that is the last element. Perfect. So it looks like that solved our issue. What we have to do now is a little element below that which is going to be called trash. So in order to do that, first thing we have to do is go inside of our terminal and let's go ahead and write npx chatcn-ui-applatest add popover. Let's go ahead and wait for this to install and then let me just zoom back in. Let's go ahead below this item right here and let's add that popover component. So before we do that, first thing I want to do is add everything we need from popover. So let's go ahead here. And let's import from ad slash components UI popover. Let's import the popover itself, the popover trigger, and the popover content like that. Perfect. And let's go all the way down here now. And let's find where we need to add that. There is it. OK, so here is I add a page and now here we write popover. Then we add a popover trigger. And let's give this a well, we didn't actually have to. I thought we might need to add as child, but I don't think we do. Let's just do a class name here with W-Fool and MarginTop of 4. And inside, let's add an item with a label of trash like that and an icon of trash as well. And let's just go ahead and import trash from Lucid React like this. And it looks like we are missing on click here in the item, but I think that that actually doesn't have to be required. So let's go ahead inside of this item props and let's make this on click optional like that. And it seems like there are no errors here. Great. Okay. There are no errors here and there are no errors here anymore. Perfect. Now below that, let's go ahead and let's add popover content. Let's give it a side and let's do dynamically. If we are on mobile, it is going to be bottom. Otherwise, it's going to be right. Like that. and let's also go ahead and give it a class name of padding0 and w-72. And for now, let's just write a paragraph which says trash box. And let me just fix the trailing white space. All right, and let's see if we have anything here. There we go. Looks like our trash box is right here. And just one thing that I want to do is I want to go inside of the popover, and I want to change its Z index. So let's go ahead inside of components UI pop hover right here. And let's find this Z index here. So this is the content. Let's change this to 99999 like that. And I think that's all that it is to change here. Great. So we just ensure that it doesn't overlap with any of the other elements we're going to have. And everything still looks the same. Perfect. So now what we have to do is actual component for the trash box. So first thing that I'm going to do is add an API route to fetch everything that is archived that should be rendered in the trash can, right? So let's go inside of convex right here. Let's go inside of documents. And let's go all the way to the bottom here. And let's write export const get trash. That's going to be a query. and it's just gonna have an asynchronous handler which holds the context. And let's just go ahead and let's copy this identity thing that we always have. So just add that to handler. So we get the identity from context out to get user identity. We check if we have it and we extract the user ID. And now let's go ahead and let's write const documents to be await context.database.query documents. dot with index by user query, query equals user ID to be user ID. And let's add a filter, which also has a query. And let's just simply write query equals query dot field is archived, is archived is true like this. So we only fetch our documents by using this index to confirm that it's made by our user ID. And then we filter so that it only shows the ones that we deleted. Let's order by descending. And let's collect all of that information. And let's return documents. All right, like that. Perfect. Let me just fix the trailing white space here. And now what I want to do is I want to create a restore mutation, right? So similar to what we did with archive, now I want to create a function to restore them, to bring them out of trash. So export const restore is going to be a mutation which has arguments ID is a type of VID documents And handler is an asynchronous function which holds the context and the arguments Let's go ahead and copy and paste this identity thing. Right here. All right. And now let's attempt to fetch the document. So const existing document is equal to await context.database.get arguments.id. If we don't have the existing document, we can return, sorry, we can throw new error, not found. And now let's check if the existing document has a matching user ID. So if existing document dot user ID, and let's just not put an exclamation point here, my apologies. So if the existing document that user ID does not match the current user ID, that means that we are not authenticated to restore this. So throw new error unauthorized. All right. And now let's go ahead and let's do a very simple const document await. Actually, let me just see. I think this is not going to do well. Just a second. Right. So what I got confused with is that I remember that we can technically recover a document which has a parent, right? So we have to create a special logic for that. So let's go ahead and let's check if our document, so if our existing document has a parent document, in that case, let's go ahead and let's write const parent to be await context.database.get document.parent document. sorry, existing document dot parent document like that. Let me try and expand this even more. All right. So if we have a parent, let's check if parent is archived. So even if the parent is archived, let's go ahead and let's change something. So in order to do this, we first have to define some options here. So const options are going to be partial document documents like that. And there's going to be an object. And this is not partial. It's partial like that. And let's give it isArchive to be false. And so if isParent is archived, in that case, options.parentDocument is going to be undefined like that. All right. And now let's go ahead and let's just do await context.database.patch arguments.id and options like that. And let's return the existing document. I like that. Great. And now we have to do a recursive restore the same way we did with recursive delete, right? So let's go ahead above these options here and let's write const recursive. restore is going to be an asynchronous function, which takes in the document ID, which is a type of ID of documents. And let's go ahead and get all the children. So const children are await context dot database dot query documents dot with index. We're getting to use by user parent. Let's get a query. Query equals user ID to be user ID, and it equals parent document to be parent document. Let's see if we... No, not parent document, it's document ID. My apologies. All right, and let's just collect that. And now let's do a four loop here. So for const child of children await context.database.patch child ID and let's give it is archive to be false. So we unarchive it. And now let's do await recursive restore to be child underscore ID. Great. And now that we have that, let's go ahead below this and let's write recursive restore to be arguments.id. Perfect. One more mutation that we have to write is the actual remove forever mutation. And that's going to be a bit simpler. So let's just write export const remove to be a mutation, which takes the arguments and an ID of we.id documents. handler is an asynchronous function which has the context and the arguments and let's just copy and paste the identity thing with the user id from our previous function like this so identity check for the identity and extraction of user id and let's go ahead and let's fetch the document we were trying to remove const existing document await context database.get arguments.id if there is no existing document, throw new error not found. And if existing document.userID is not identical to currently logged in user ID, in that case, we can throw unauthorized like this. Great. And now that we solved that, we can just simply go ahead and do const document to be await context.database.deletearguments.id like this and return the document. Perfect. And I think that should be it. And let's just change this one. So go back inside of this restore function here. And I just want to do this one simple thing. I don't think it really matters. But we are returning this existing document, which we fetch all the way here, right? So we don't actually modify that element. Even though I think this should work, what I want to do is just const document here. And then just return that document instead. Like this, very simple. Great. So now that we have that, we are finally ready to create our trash box component. Let's go back inside of the navigation. So app folder main components navigation right here. And inside we have a paragraph. Instead of this popover and the popover content, we wrote a paragraph for trash box. Let's change it to be an actual trash box like this. If we save, of course, we're going to get some errors. So let's go inside of this underscore components and create a new file trash-box.dsx. Let's mark this as use client. And let's export const trash box to very simply return a div saying trash box like that. Go back inside of navigation and import the trash box from dot slash trash-box. And there we go. we no longer have an error. And now we can fully focus on this right here. So let's go ahead and let's add some hooks first. So const router, whoops, const router from use router from next slash navigation. Const params, use params from next slash navigation. Const documents is going to be use query, which we have to import from convex react. so use query and let's add use mutation while we are here from convex slash react and let's add the api from at slash convex underscore generated api so for the documents we're going to use api.document dot get trash that's the function we're going to use now let's add the restore mutation so const restore is use mutation api.document.restore the function we just wrote and let's also do remove, const remove, use mutation, api.documents.remove. Great. Now let's go ahead and let's add a search state because we want to be able to search through the entire trash box. So const search, set search, whoops, set search from use state. And let's give it the default value of an empty string and make sure you import your state from react and now let's go ahead and let's write const filtered documents to be documents question mark dot filter let's get the individual document and let's return document dot title dot to lower case dot includes search dot to lower case as well so a quick little function to filter out the documents using this search state right here. And now let's just add a const on click function to get the individual document ID, which is a type of string. And let's just do router dot push backticks documents, document ID. So that page that we don't yet have. All right. And now let's go ahead and let's add two more functions. So functions for restore and for delete. So const on restore is going to have an event which is a type of react.mouse event html div element mouse event as well. And it's mouse event. And it's also going to have the document ID which we are trying to restore, which is a type of ID. And you can get that from convex underscore generated data model. And it's going to be an ID of documents like that. Great. And now let's open this arrow function here. And let's stop the propagation from the event. And let's write const promise to be restore. And let's pass in the ID to be document ID. and now let's get our toast from sonar so import toast from sonar and let's go ahead now and let write post let pass in this promise let give it a loading state of restoring note Let go ahead and give it a success state of note restored And let's give it an error of failed to restore note. There we go. And now let's go ahead and let's copy and paste this entire thing. and let's rename it to on remove. And this one is not going to take the event. So it's just going to take a document ID. So no need to stop propagation here. And let's just change this promise to not use restore, but instead to use remove by document ID. And this is not going to be restoring note. It's going to be deleting note. And this is going to be note deleted. And this is going to be failed to delete note. Great. And now let's just do if params.documentId is equal to documentId. So if we are looking at the document, which we just deleted forever, we want to redirect back away from that document. Otherwise, we're going to get an error because we can no longer load that page. So we're going to do router.push slash documents, just like that. Great. Now that we have that, let's go ahead and let's add our loading state here. So if documents is undefined, that means it's still loading. So we can go ahead and return a div with a spinner component. So import this spinner from add slash components spinner. Give it a size of large and give it a class name of h-full flex items-center. justified dash center and padding of four. Great. So now what I want to do is actually show the list of our items in trash here. Before we do that, let's go quickly inside of our terminal here and let's go ahead and write npx chat cnui at latest add input. So we're going to need the input component for this one. Great. Make sure you have that. Let's zoom back in and let's give this div a class name of text small. Let's open a new div with a class name of flex items dash center gap dash x dash one and padding off two. Let's render the search icon inside. We can get the search icon from lucid react. So make sure you import search from lucid react. So let me just move it here. Where were we? Okay, right here. And let's give this search icon a class name of h-4 and w-4. And below that, let's add our new input component, which we just added. And that's located at add slash components slash UI slash input. And let me just show you that right here. There we go. Make sure you add that. Perfect. And let's go ahead and give this a value of our search state. Let's give it an on change to take the event. And let's just do set search to be event.target.value. And let's give it a class name of h-7 padding x of 2, focus-visible, ring-transparent, and bg-secondary. And let's give it a placeholder of filter by page title. All right. And now let's go outside of this div and let's open a new one. And let's give this one a class name of marginCop2, px1 and pb1. And let's add a paragraph here, no documents found. And let's only show that paragraph if there is nothing else rendered inside. And we can do that using CSS. So let's add a class name hidden by default. and it's only visible if it's the last element inside of this list like that and it's going to have a extra small text it's going to be centered text as well and it's going to be muted foreground color and padding bottom off too let's see if we can already see any of those let me just refresh this let's click on this trash icon and there we go you can see how it loads for a second we have a little search here and it says no documents found here perfect and now we have to render the documents inside. So let's go ahead and do that. So just below this paragraph right here, go ahead and open filtered documents, question mark, dot map, get the individual document and render a div. And let's give this div a key of document dot underscore ID. all right let's give it a roll off button let's give it an on click to be a narrow function which calls on click and document underscore id and we have this on click defined right here so when we click on it we're going to redirect to it all right and then let's go ahead and give it a class name of text small rounded small w pool power bg dash primary slash five flags items dash center next primary and justify between and inside let's go ahead and let's render our span element which is going to render the document title all right and let's see if we are already seeing that here there we go look at all of our deleted items right here and if you click it should lead you to a 404 page because we don't have it yet. Perfect. So now we're gonna style this a bit and we're going to add some actions beside it to restore it back or to delete it forever. Great. So go below this span right here and create a div here with a class name of truncate in case the title is too long. PL-2. Oh my apologies. This is actually a class name that I wanted to give to this span like that and for this div just a class name of a simple flex item center and inside open a div which is going to have an on click function get the event and call on restore pass in the event but also pass in the document underscore id and give this a roll of button and a class name of rounded small padding 2 and hover bg neutral 200. Great. And inside, we're going to render the undo icon from lucid react. So make sure you import undo from lucid react. Let's see how that looks now. When I click on trash. Okay, we have a big undo button on the side here. So now let's just style that button a bit. so let's go ahead and give it a class name height four with four and text muted foreground like that perfect and now go below that and add a new div which is going to render the trash can from lucid react so let's go ahead and import trash from lucid react all right let's give it a class name of height four with four and text muted foreground and let's go ahead and give this div a roll of button and the class name of rounded small padding two and hover bg neutral 200 like that and now we should have two little icons here there we go the trash icon and the restore button And I think our restore should already be working. There we go. You can see how when I restore, they all become individual items because they don't have a parent, right? And let's go ahead and delete them individually. And we can do that as well. Perfect. And let me just delete absolutely everything from my table so I can try this out even more. So I'm going to try this. I'm going to add a new parent inside. There we go. So I have three elements. I have nothing in trash. I'm going to delete the parent like this. And I think that this is the parent, the first one that was deleted. And I'm just gonna attempt to restore that one. And everything else should be restored as well. Let's see, there we go. Everything else is restored inside. Perfect. So what's left to do now, let me have something deleted, is the actual delete function. Right now, it doesn't do anything beside actually redirect me to that page. So that's what I have to do next. So to wrap it up, we have to add the complete remove function. And in order to do that, I don't want to assign it to just be on click. I want to have a nice little model so the user can confirm that they want to do this because this is not a reversible action. For just archiving, we don't need a confirmation model because they can always go to the trash box and bring it back. But for the absolute complete removal from the database, I think it's nice that we create a little confirmation model for that. So let's go inside of our terminal and let's install a package. mpx shadcn-ui at latest add alert-dialogue. Go ahead and install this package. And after that is done, we're going to create a reusable component called confirm model. So let's go inside of our components folder and create a new folder called models. and inside create a new file confirm-model.tsx like this. Let's go ahead and mark this as use client. And let's go ahead and let's import everything we need from add slash components UI alert dialog, which we just installed. So we need the dialog itself. We need the dialog action. We need the cancel action as well. We need the content. We need the description. putter, header, whoops. So I alert dialog header and title and last one trigger. So all of this inside. Perfect. And let's go ahead and write an interface for this. So interface confirm model props is going to accept children, which are react.react node. and we're going to have the on confirm option, which is going to be, well, whatever we want, depending on the use case. And now let's export const confirm model. And let's go ahead and assign this props here. So confirm model props and let's extract the children and the on confirm. Great.\nAnd now let's go ahead and let's just do a return of the alert dialog. Let's add the alert dialog trigger. And let's random the children inside. So the trigger is going to be whatever we want it to. Whatever we wrap the confirm model around, that's going to be the trigger button. And let's add the on click, which just simply is going to call event.stoppropagation. And let's give the as child option. Perfect. Now inside, let's add the alert dialog content. Great. Alert dialog header. And let's go ahead and write the alert dialog title, which is going to have the title of Are You Absolutely Sure? And below the title, we're going to add the alert dialog description, which is just going to confirm to the user that this is an irreversible action. So this action cannot be undone. And now let's go outside of the header and let's add the alert dialog footer. And let's just go ahead and write the alert dialog cancel button here. whoops it's not going to be a self-closing tag like this and let's go inside and let's just write cancel and let's give it an on click event event.stoppropagation like that and now let's add the alert dialogue action and this one let's also give it an ending tag and this one is going to say confirm like this and now let's just go ahead and write a function handle confirm so const handle confirm is going to be an event of react.mouse event html button element and mouse event like this let me just put this in a separate line so that's going to be our event and let's do event stop propagation and let's do on confirm right here. And then we can use this handle confirm to be an on click for this action. And now we can go back inside of our trash box here, and we can wrap this entire thing inside a confirm model like this. So make sure you import confirm model from add slash components models confirm model, wrap the entire div around it, and just give this one on confirm to be a function, which is going to remove the document. So it's going to be an arrow function which calls on remove document ID like that. And we have on removed right here. Perfect. Now let's go ahead and try that out. So I have three items in the database. Let's see. Two of them are active. One is in the trash. If I click here, oh, it looks like I have a model. But as you can see, it's not, the Z index is not correct. So before we try this out, let's actually fix the Z index first. So we have to go inside of components UI alert dialog. And let's find the Z index for the overlay and change the 50 to five nines. So one, two, three, one, two, like that. And let's copy and paste this. So we did it for the overlay. And now let's also just do it for the content. So let's just find Z index right here. There we go. let's add it here let's see if we have the index anywhere else I think that's it yeah great so just those two places and let's try this again when I click here there we go you can see how now it's at top and let's just not click at anything else while we're doing this and let's click confirm and there we go note is deleted and it's no longer in trash and if I look at my database I only have two items. Perfect. And let's go ahead and try this one more time. There we go. I can now completely remove my entire database just by using UI. Amazing, amazing job. So now let's go ahead and let's wrap up our sidebar items right here. So we have our working pages here. We have the working trash, but we don't have the working settings and the search bar. So before I get onto that, I just want to bring to your attention that currently, when I expand here, you can see how for a second, it's like I have a blank space, and then I have something rendered. Instead of that, I was hoping that the skeleton would show because we did create an item skeleton right here in our app folder, main components item. You can see that right here, we have a skeleton right here, but looks like something's not working. so what I want to do is go inside the components UI and find the skeleton right here and it says that this background is muted so let's go ahead and let's change that to be bg primary slash five like this let's change that to this and let's see if that improves anything there we go you can see how now for a second it's very subtle but if you look at it I have like a small like a small little a skeleton inside instead of just a blank space. And in my opinion, that looks better. Perfect. So now let's go ahead and let's create the search functionality for that. Let's go ahead and let's close everything. And first things first, let's go ahead and let's install Sustent. So npm install Sustent. We're going to use that as our global state management to open the actual search command. So now let's go ahead and let's create a hook for that. So go inside of your hooks folder and create a new file, use-search.tsx, like that. And go ahead and import create from Sustan. And go ahead and type search store to have is open, which is a Boolean. It's going to have on open, which is going to be a void. It's going to have on close, which is also going to be a void. And we're also going to have a toggle, which is going to be a void as well. And then let's export const use search to be create with a type of search store. And go ahead and open parentheses, open parentheses again and extract the set and get options. And go ahead and return an immediate object like this and press enter. And let's give it a default value of is open to false. On open to be a function, an arrow function which calls the set and just uses the is open to be true. on close is also an arrow function which uses set to reset is open back to false and last one is going to be toggle which is an arrow function which uses set again and it's going to take the current value of this open and do the reverse of it so is open is going to be question mark get is open like this there we go perfect so now that we have this let's go ahead and let's install the command from ShatCN UI. So go back inside of your terminal here and go ahead and write npx ShatCN-UI at latest add command like this. Let's wait a second for this to install and just a couple of seconds more and install the dialog as well. Great. and now let's go ahead and zoom back in and let's go inside of our global components folder and create a new file called search-command.dsx and let me expand my sidebar so you can see this so search-command.dsx perfect let's go ahead and mark this as use client and let's import use effect and use state from React. Let's go ahead and import file from Lucid React. Let's go ahead and import use query from convex slash React. Let's go ahead and import use router from next slash navigation. And let's go ahead and let's import use user from add clerk slash clerk dash react. Great. And now let's go ahead and import everything we need from our new component, command. We're going to have the command dialog. We're going to have the command empty, the command group, input, command input. We're going to get the command item and the command list. Great. Besides that, we're also going to import that hook, which we just created, called use search from add slash hooks use dash search and we're also going to import our api from convex underscore generated api great and now let's export const search command to be an arrow function and let's go ahead and let's write a constant user we use user let's go ahead and let's get the router from use router let's go ahead and fetch the documents and let's write use query. And I just remembered that we actually did not write the function, the API endpoint to load all the documents needed for search. We have the thing to load for sidebar, but for search is going to be a bit different. So let's head back inside of convex, inside the documents.ds right here. And let's go all the way to the bottom here. And let's add a new endpoint, export const get search, which is going to be a query. It's going to take a handler, which is an asynchronous function, which has the context. And let's just copy and paste the identity thing as we always do and copy the user ID as well. Just like that. And let's write const documents to be await context.database.query documents.withindex by user and passing the query, which is just going to search for a query that equals user ID to be user ID. And let's filter. Let's get the query again. And the other thing we're going to filter is query equals query.field is archived to be false. So we are not going to be able to search through archived files. For that, we have the trash box. Let me just fix the trailing white space here. And let's add order to be descending. And let's just collect everything. Like that. Perfect. And then we can just return documents That it That all we need for our get search endpoint Now let go back inside of the search command And let use that in this query So API documents dot get search. There we go. And now let's get our toggle. So const toggle is equal use search, get the store and write store dot toggle. Let's copy and paste this two times. Let's change this one to be is open from the store and let's change this one to be on close from the store. Great and now let's go ahead below these documents and let's add a state is mounted and set is mounted to be use state false and now let's write a use effect to change this mounted to true and go ahead and give it an empty dependency array. so the reason we are doing this is to prevent hydration errors with stuff like dialogues and command uses dialogue in the background so the reason we are doing this is because even though this is marked as use client next js is still going to do some server side rendering on it first and with dialogues which can appear dynamically for example our search command can appear on a shortcut right that can cause hydration errors because on server side it's not going to exist and then it reaches client side and all of a sudden it exists here and that's called a hydration error. So we're going to prevent that by not even allowing it to be rendered on the server side even though it already has use client that's not enough. So use client still does some server side rendering right. So that's why we have to use this trick that only when it's mounted we're actually going to render anything because we're going to add if is not mounted just return no. And this way we prevent that hydration error. Perfect. So now let's go ahead and let's go and return our actual command here. So command dialog. It's going to have open of is open and on open change is going to be on close. Go inside and add the command input component. And go ahead and give it placeholder to be open backticks like this. And we're going to write search open special object user question mark full name apostrophe S Joshan. So search Antonius Joshan in my case, right? And then go ahead and end the command list and add the command empty, which is just going to render no results, results found like that. and then add a command group and give the command group a heading of documents. And let's go ahead and let's write documents.map, question mark.map. Let's get the individual document like that. I can just immediately return like this. And let's return a command item. And first things first, let's go ahead and give the command item some props. So we're going to have a key, which is going to be document underscore ID. Let's go ahead and give it a value of open backticks, a combination of document ID dash document dot title, because what we searched through is the value. So we have to combine both of those. And then let's go ahead and give it a title of document.title. Let's write on select for now to just be an empty arrow function. And we already added the key. That's it. And inside, let's dynamically check for the document icon. So if document has an icon, in that case, we're going to render a paragraph with document.icon. and let's just go ahead and let's give this a class name of margin write 2 and text 18 pixels otherwise we're going to go ahead and render the file icon from lucid react which we just imported and add a class name of margin write 2 height 4 and w4 great and below all of that add a span, which is just going to render document.title. Perfect. And now let's go ahead and let's create this on select function right here. So go above this if clause and write const on select to have an ID of string and router.push to slash documents slash ID individual document and on close after that happens. Perfect. And now let's go ahead and let's create a use effect. So we have a shortcut so we can open this search dialog whenever we want so let's do that close to this use effect that we already have here let's just prepare an empty use effect for now and let's write const down which is going to have an event of keyboard event and let's check if event dot key is equal to k and if e is a meta key or or e is control key so we handle both windows and macbooks here in that case let's prevent default and let's just toggle whether it's opened or not and lastly let's do document dot add event listener key down and let's do the down function here and now let's do a return so on unmount we have to remove the event listener to prevent any overflow so document.removeEventListener key down and down function and we have to add toggle inside of the dependency array and now let's copy this use on select here and let's replace it right here there we go perfect and now we have to go and add this search command inside of our layout but not just any layout. We have to add it inside of app main layout.tsx right here. And let's go ahead above this children, but still inside of main and render the search command. So we imported search command from add slash components search command. And I think that already if you try, and let me refresh, if I press command key, there we go. You can see that I have my dialogue opened. So if you don't, make sure that you try control key and just confirm with my code that your use effect is working. And as you can see, it's working, but you can see that the sidebar is not blurred. So that's because of the Z index thing. So let's go ahead and let's revisit our components UI. And let's see what we have to change here. So I think we actually don't have to change anything inside of the command. Let's see. I think we don't. Yeah, I think what we have to do is change it inside of dialog.tsx. So let's go ahead inside of the dialog here and find this index 50 and change it to five nines like this. Go ahead and copy this and do the same thing for the content right here. So we did it for the overlay and we did it for the content and I think that's it. I don't think anything else needs it. And if I try now, there we go. You can see how now even the sidebar is blurred and I can search for, well, everything is named the same, but you can try and search something completely different to get a wrong thing. And if you click on one of them, you're going to get a 404 because we don't have that yet. Perfect. So now what we have to do is enable that when we click on this, that happens as well. And thankfully we can do that very, very easily. We can just go inside of the app folder, main components, navigation right here. And while we're here, let's import use search from hooks, use search like that. And let's add that inside of here. So const search is equal use search like that. And then let's go ahead and find our search item. There we go. It's right here. And on click, change this to just be search dot on open, just like this. And let's try that out now. When I click here, there we go. It opens. So I can use the shortcut or I can click on it right here. Great. So we're going to do a very similar thing to enable the settings. And now let's create the last item in the sidebar, which is this settings right here. So we're going to use a very similar method to what we did with the search right here. We're going to create a hook called user settings, which is going to use to stand to create a global store to track whether we have clicked on this or not. And that's going to toggle the is open Boolean to true or false. And depending on that, we're going to keep that component opened or not. So let's start by creating our hook. So let's go inside of hooks and let's go ahead and create use-settings.tsx. Let's go ahead and let's import create from to stand and let's write type settings store and very similar to search store is going to have is open, which is a boolean, is going to have on open, which is just a void and on close, which is also just a void. but it's not going to have a toggle. And let's export const use settings here. To be create, pass in the settings store and go ahead and extract the set, well, just the set function and go ahead and return an immediate object. Set is open to have a default value or false. On open to be a function, sorry, not void, but call a set function, which is going to set that is open to true, and copy and paste this, and change this one to be on close, and that one is going to set is open to false. Great, and now let's go ahead and let's create our settings model, so save this file, close everything, go inside of components, models, and create a new file, settings-model.tsx, and let's go ahead and let's mark everything as use client inside, And let's go ahead and let's import everything we need from add slash components UI dialog. So we never actually installed the dialog explicitly, right? But we did get it installed when we installed the command component for the search command right here. When we installed these things, you can see that the command uses the dialog, right? But just in case, if you don't have the dialog, so if you can't find that inside of your UI folder, this dialog right here, even though you should have it because I think in the previous part we changed the Z index here right But just in case you don have it you can always just go into your terminal and do npx chat cnui at latest add dialog like this I'm not going to do it because we already have it. All right. Let me zoom back in. And let's import the dialog, the dialog content, and the dialog header. And let's go ahead and let's import use settings from hooks, use settings. and well, we need a label. So let's go ahead and let's do that by going inside of our terminal. I shut it down too soon and let's write npx chatcn-ui at latest add label like this. Wait a second for this to install and once it's done, we can go back into the code here and let's import the label from add slash components slash UI label. and lastly, I want to import the mode toggle component from, well, it auto-completed for this, but I'm going to change it to add slash components, so we already have mode toggle, if you remember, we have that inside of our navigation bar when we log out, so let me just log out from here, and there we go, this is the mode toggle component, which can switch to light and dark mode. So let me just head back inside of my component here. Let me go back. Okay. And now that we confirmed that we have that mode toggle, so you can also just control click and go here to confirm that you have it. And now let's go ahead and let's write export const settings model like that let's write const settings to be use settings and let's return a dialogue component let's go ahead and let's give this dialogue an open prop of settings is open and on open change to be settings.onclose let's go ahead and render the dialogue content and let's render the dialogue header let's give it a class name of border bottom and padding bottom of three let's add an h2 element which is just going to say my settings like that let me just collapse that so we can safely add a class name here without taking much of my space here so the class name is going to have a large text and a semi-bold font actually let's do medium semi-bold might be a bit too much great and now below this dialog header open up a div and let's give this a flex so flex items dash center and justify dash between and on this side we're gonna have another div which is going to be a flex column so flex flex dash call and gap y one and let's give it a label here which we imported above with appearance and a span, which is just going to say, customize how Joshun looks on your device. And let's give this a class name of text 0.8 rem and text-muted-foreground. All right. And now let's go outside of this div right here and let's render a mode toggle. just like this. Perfect. And now, if you remember in the settings, in the, so not in the settings, in the search command, where is it? Let me close this. In the search command, we do this trick with mounting, right? But it looks like I didn't do it here in the settings model. Why is that? It's the very same logic. It uses the model, right? And it uses to stand for the state. Well, you're correct, but we're not going to do it here because we're going to have another model in this application. And we can do a little trick with a common component to do that for us. So we can just leave the settings model as it is. And instead, let's go ahead inside of our providers. So I'm gonna close everything. Go inside of components, providers here and create a new file model-provider.tsx. Mark this as use client. Go ahead and import use effect from React and use state from React. And now let's go ahead and let's import the settings model. And I'm just going to change this to be add components. It doesn't really matter, but I like to be consistent. And now let's do export const model provider. And let's go ahead and let's return a fragment. Whoops, what did they do? Let's return a fragment, which is going to render a list of our models. The only one we have right now is the settings model. So we can just do this. And now here, we're going to write the isMounted, setIsMounted. We use state false by default and the use effect here. Let's start with an empty one and let's just write setIsMounted to true. So now none of the models are going to be rendered unless we are fully on the client side. So on server side, we're just not going to render any models. That's completely fine. We don't need them to be optimized or anything. and that's not going to cause any hydration errors. And let's just do if is not mounted, return now. Great. And now that we have the model provider, we also have to add that model provider to our layout. So let me just save this and let's go inside of our app layout.psx and let's go ahead and add it below the toaster here. So model provider and make sure you input a model provider from here. So I'm just gonna keep my providers together like that. So I have these three providers. Let me move this to the top and let me have this all the way here. There we go. So let me just show you. You should have these three providers. Model providers is the one we just added here and we rendered it right here. Model provider, perfect. And now, well, nothing is still shown here, right? So if I click, nothing happens. So let's go back inside of our app folder, main components navigation. And just the same way we did add the use search, go ahead and copy and paste this and rename this to use settings and change this to use settings. And then you can use that here in the same way, const settings, use settings, and go ahead and find the settings item the same way we did with the search item. It's right here. There we go. So just below the search and change this to be settings dot on open. On open. Whoops, like this. There we go. And let's try it out. Now. If I go into settings, there we go. Let me see if I can click on dark mode. And there we go. Now everything here is on dark mode. And it looks like we have a bit of issues here. So let's see, first thing I'm noticing is that in dark mode, this seems to have its own little border here, which is just weird. So we're going to have to find a way to fix that. Let's see how the search looks. Search looks fine. Let's see how this looks. This looks fine as well. The trash looks fine, but let's see if that's true when I delete something. so let's go ahead oh look at this as you can see this one's look uh too white for the dark mode so let's go ahead and quickly resolve that first so have something in your trash and let's go ahead and let's revisit our trash component so inside of app main components trash box right here let's find where that is so it's those two items right here so find the confirm model and find this div with the roll button. So these two ones are our suspects and looks like we put this hover color on light mode, but let's add dark hover. In that case, BG is neutral 600, like that. And let's copy and paste this and add it for this div as well, which has the roll button, which has the trash icon. So let me just add a space here and paste that. Let's see if that improved anything. So I'm going to go back here. There we go. That looks a bit better now. It's not as shiny as it was before. Let's just confirm. Yeah, I think that looks okay. It kind of looks the same when I hover here. That's fine. And let me just see what's going on with this right here. So we have to go back inside of our item component for this. Let me close this. Let's find the app main components item. and let's find the chevron icon because that's the one that we need here whereas this is the chevron icon and we render it right here let's see the class names so h full rounded small hover bg neutral and then on dark oh it looks like we added the bg neutral like this it's missing a dark hover like this dark colon hover colon let's try it out now and there we go now it looks a bit better perfect so this is how I wanted it to look like perfect and as you can see everything looks great here great great job so now we are ready to continue working here and now we're gonna go ahead and work on this navbar so when we click on this first we're gonna get rid of this 404 error and we're not gonna display anything yet but we're just gonna be working on the navbar so we can finally rename our documents. And you're going to see how cool it is when it happens in real time using the convex database. So now that we have our sidebar finished, we have the working search, we have the working settings, let's go ahead and let's fix this 404 page, which happens when we click on a specific document from our sidebar here. So in order to fix that, let's go ahead Inside of our app folder, main, routes, documents, and inside, create another folder in square brackets, document ID like this. And inside, create a new file, page.dsx. And let's go ahead and name this document ID page. And for now, let's just return document ID inside. And already, if you save this, it should be enough to prevent any errors when I click here. There we go. Now it says document ID. So if you're wondering how this works, well, look at the URL that I have. It's localhost 3000 slash documents slash the ID of my document right And let take a look at the structure we created So we have a folder documents and then we have document ID So by using square brackets we made this a route which can be dynamic So it can be any ID that we want it to be. And that's why we can, that's why this works. If we remove the brackets, then it would expect it to literally be document ID. And that's not what we want. So that's why the square brackets are important. Great. So we're going to leave it like that for now. And instead, we're going to go ahead and focus on creating the navigation bar, which is going to be right here. And it's going to be only visible when we click on a specific item. So let's go ahead back inside of our main folder here inside of components and inside of navigation.tsx. And let's go ahead here and find we already have one navigation here, but this is only a placeholder while nothing is active, right? It's only used to collapse a specific menu item. So let's go ahead now and let's add the params in here. Perhaps we already have them. We don't. So let's go ahead and write const params to be use params. And you can import use params from next slash navigation. So just make sure you add use params from next slash navigation. And now that we have params, we can go all the way down here and turn this into a dynamic view. So let's go ahead and make sure you're inside of this div, which holds the navbar ref. And then we're dynamically going to render the content. So if we have params.documentID, so we're using this double exclamation points to turn this string into a Boolean. So if we have that, in that case, we're going to go ahead and render a navbar component, which we don't yet have, which is going to have a prop is collapsed to be is collapsed, which we hold in a state. And on reset width, it's going to be reset width like that. Otherwise, if we don't have it, we're just going to render this current navbar, which we have right here, which just simply holds the menu icon to expand the menu on mobile mode. And if you save, of course, you're going to get an error. So now let's go ahead inside of this underscore components and let's create a new file navbar.tsx and let's export const navbar and let's just return a div saying navbar. Now we can go back inside of our navigation.tsx and we can import that from dot slash underscore navbar. Make sure you don't accidentally import this one from the marketing page. So this new one which we created, which is in the same folder. And let me show you how that looks. So dot slash navbar. Make sure you have that. And as you can see, now I have some overlapping text right here because it's both the navbar and the document ID, which I've written in the document ID page. But let's not worry about that now. And instead, let's go back inside of navbar here and let's go ahead and mark it as use client. And then let's write interface navbar props to be is collapsed, Boolean, and on reset width to be a void. And let's go ahead and just assign those props here. So I'm going to extract is collapsed and on reset width, and I'm going to map them to navbar props. And now if you go back into navigation, you should no longer have any errors regarding of the rendering of this navbar component. Now let's go back inside of our convex API because we need to add a specific API which we're going to use inside of this navbar function here. So let's go inside of the convex folder, inside of the documents.ds and let's go all the way to the bottom here and let's go ahead and write export const get by id. That's going to be a query which has the arguments of document id which is a type of v.id documents. Besides the arguments, we're also going to have a handler, which is an asynchronous function, which has the context and the arguments we pass in above. So as always, first, let's go ahead and let's get our identity. So const identity is equal to await context.out.getUserIdentity. And now we don't have to check for whether we have the identity or not, because we're going to reuse this API route for guests to view your published notes, right? If you remember from the demo, we're going to have a functionality where you will be able to publish your notes to the web. And then everyone who has a link will be able to see them even in incognito mode without them being logged in. So we're not just going to check for the identity here. We're just going to extract it. And instead, we're immediately going to fetch the document. So const document is equal to await context.database.get arguments.documentId. And first things first, let's check if this even exists. So if we don't have the document, we can go ahead and throw new error, not found. And now let's check if we can already show this document to the user. And the way we know that we can show this document even without checking whether we are logged in or not is if the document is published. So if we publish the documents, that means we can already return it. But I just want to do one quick thing. I want to confirm that by any chance it's not archived. So let's also confirm the document is not deleted from the side of the creator. And then we can safely return the document. And if this is not the case, in that case, we continue with our checks. And the first check we have to do if we have the identity after all. If we don't have it, throw new error, not authenticated. And now let's go ahead and let's extract the user ID, const user ID to be identity.subject. And let's go ahead and write if document.userID is not equal, it's not identical to the user ID which just extracted. In that case, it means we are not authorized. So unauthorized to view this. And otherwise, return the document. Great. So now that we have this API endpoint, we can head back inside of our navbar here. And let's go ahead and let's import useQuery from convexReact like that. And we also have to add our params. So use params from max slash navigation. And now let's go ahead and let's extract the params. And now we can do the query. So const document is equal use query API dot document. Oops, let me show you what I'm writing. I didn't import API. So let's import API from add slash context underscore generated slash API right here. So just as I did it here, I'm just going to separate this imports. and then we can just autocomplete API documents get by ID and we have to pass in the argument which is the document ID and that's going to be params.documentID but now we're going to have a type error here because params.documentID is a string but what we expect is a type of ID document so we can just simply fix this by writing as ID and we have to import that from at slash convex slash underscore generated data model so make sure you add this import as well and extract the ID. And then we can just write ID documents. And there we go. No errors here. Perfect. Now let's go ahead and let's write if document is undefined. For now, we're just going to return a paragraph saying loading. Later, we're going to change that to be a proper skeleton. Perfect. And now let's go ahead and let's just write if by chance the document is null. We're going to return null as well. otherwise let's go ahead and let's change this to be a fragment like that let's add the navigation element and let's give it a class name of bg-background let's give it a dark of bg-hexcode 1f1f1f px3py2w-full flex items-center and gap-x-4. Great. So now that we have that, let's go inside of here and let's dynamically render our is collapsed. In that case, go ahead and render the menu icon from Lucid React. So make sure you add the menu icon from Lucid React. I'm going to move it with this global imports at the top. And let's go ahead and let's give this one a roll of button. Let's give it an on click on on reset with. And let's give it a class name of h-6w-6 and text-muted-foreground. Great. And now outside of this dynamic conditional rendering, open up a div with a class name. which is going to be flex items dash center justify dash between and w dash full like that. And let's just write navbar here. And there we go. Let's take a look at how this looks now. As you can see on desktop, it just says navbar. And as you can see right now, it looks like our document ID text has disappeared, but it's still here. It's just that we've given this a white background. So it's no longer visible, but don't worry. When we come back to the document, we're going to properly align all that. And as you can see, when I completely collapse this, I have a nice little menu here. And if I go ahead and delete this, you can see how it still says navbar here. So let's see if that's, oh, okay, because the URL is still here. So let's manually just change the slash documents like this. There we go. And if I collapse now, you can see how we still have all of that. And if we go here, then you can see how it's next to the navbar here, because it's going to load that specific document here. Perfect. So make sure you click on a specific document, make sure you have the navbar here. And what we're going to do now is we're going to create a little component called the title so the user can actually change the title from here. So inside of this nav element right here, let's go ahead and replace this text which says navbar with a title component, which is going to have initial data to be document, which we fetch from this query right here. And if you save, of course, you're going to get an error because title does not exist as a component yet. So let's go ahead and quickly create that right here in the underscore components, where our navbar and navigation is create a title.vsx. Let's go ahead and modify.\nthis as use client as well and let's export constitle and return a div saying title. Now we can go back to the navbar and we can import that. Make sure you don't import from any of this so just dot slash title. It's in the same folder so just like that no need to complicate it further. Now let's go back inside of the title component and let's give it some props. So interface, title, props, initial data is a type of document which we get from at slash convex underscore generated data model. And let's go ahead and extract documents like that. And let's go ahead and extract that here. So title, props, and initial data. Great. So now what we have to do is create another API endpoint, which is going to be used for updating a document. Since we're going to change the title here, we have to prepare that endpoint. So let's go back inside of our documents in the convex folder right here. Let's go all the way down and let's go ahead and write export const update to the mutation, which has the following arguments. It will have an ID, which is a type of ID documents. it's going to have the title, which is an optional value. So not always are we going to have to pass that. And it's a type of V dot string. We're also going to have the content, which is also optional and also a type of string. We're going to have a cover image, which is also going to be optional and the type of string. Besides the cover image, we're also going to have an icon, which is also an optional string. And last one is going to be is published, which is an optional Boolean. Great. And now let's get our handler, which is an asynchronous function, which has the context and the arguments. And let's go ahead and let's start by checking if we have the identities. So const identity is equal to await context.arguments. Sorry, context.out.getUserIdentity. If there is no identity, in that case, throw new error, unauthenticated. Okay. And now let's extract the user ID to be identity.subject. And now let's prepare our data from these arguments right here. So I'm going to go ahead and write const from the arguments. and I'm going to extract the ID and then I'm just going to keep everything as a rest. So I'm destructuring everything inside of these arguments and that's all of this stuff. And I'm purposely extracting the ID because we're never going to send the ID to be updated. We're just going to use it to find what we have to update. But with this one, we can just pass in the rest in the patch function and that's going to update whatever we send it. So if we sent the title, title is going to get updated. If we sent the content, content. And same for all the other things. So we kind of eliminated or omitted the ID from the arguments object by creating a destructuring method here where we just kept everything else inside of a constant name rest and extracted ID separately. All right. And now let's fetch the document. So const existing document is going to be await context.database.get arguments.id. and now let's go if there is no existing document throw new error not found and let's check if we have the authorization to do this so if the existing document dot user id is not identical to the user id we extracted using the identity subject in that case throw new error unauthorized. And now we can just do const document to be await context.database.patch arguments.id and just spread rest and return the document. Just like that, we have our patch function here. Perfect. So we're going to use this function update every time we need to update something. And the first place we're going to use it is inside of the title component. So let's go ahead and let's import useMutation from convexReact. And let's also import our API from convexGeneratedAPI. And let's go ahead and let's write const update to be useMutationAPI.documents. And it's right here, update. Great. So now that we have that, let's go ahead and let's actually render something here. So in this div, we're going to go ahead and give it a class name of flex items that are center and gap X one. We're going to go ahead and render the icon if we have it. So if inside of the initial data, we have the icon in that case, go ahead and render a paragraph. Whoops, my apologies. And inside, we're just going to render the initial data dot icon. That simple. And now let's go ahead and write, well, first we have to add a state called isEditing. So let's go ahead and do that. Const isEditing and the set isEditing. It's gonna be useState from React with a default value of false. So let me just move this to the top right here. Great. And now let's go ahead and let's dynamically render using this isEditing boolean right here. So if we're editing, then we're going to render the input component from add slash components UI input. Make sure you have this imported from chat CN like this. And let's go ahead and let's give it well for now we can just give it a class name of h-7 px-2 and focus-visible to be ring-transparent like that. And we don't have to give it anything else for now. And let's go ahead and write the else function here. Instead, we're going to render a button like that. So make sure you import button from add slash components UI button. And what we're going to do is we're going to render the initial data dot title like this inside. And let's go ahead and give this button an on click, which is just going to be an empty arrow function for now, a variant of ghost, a size of small, and the class name of font-normal, age-auto, and padding of one. And let's wrap this inside of a span. And let's write a class name truncate. Great. So now if you take a look here, this becomes a little button. And when we click on it, we're going to change this is editing state to true. And then that's going to turn into an input. So let's go ahead and play around with that. So let's add some refs that we need here and some functions. So I'm going to write const input ref here to be use ref from react, make sure you import use ref right here. And let's give it a type of HTML input element with a default value of null. Great. Now let's go ahead and let's create the title state. So const title, set title is use state. And the default value is initial data.title or untitled. Just in case we are not able to load it. And let's write const enable input to be set title to initial data.title in case it has changed in that time. and then set is editing to be true and set timeout with a timeout of zero so we're just using a little hack here because i want to focus on the input and to do that we're going to use the input ref dot current question mark focus and we're going to input ref that current question mark set selection range to start from zero and end by the entire length of the value. Great. And now that we have that, let's go ahead and let's add a couple of more functions which we need here. So we're going to need the disable input. Const disable input is going to be a function which just calls set is editing back to false. So very simple, but we're going to use it in a function so it's easier to call. And now let's do const on change to be a type of event which has the react.change event HTML input element like that. And let's set title locally to be event.target.value. And then let's go ahead and let's call our update function, our update mutation to have the ID of initial data dot underscore id and let's pass in the title to be event dot target dot value or untitled so if we try to delete everything it's automatically going to go back to untitled we're never going to allow the user to delete the entire title if they remove everything it's going to be untitled all right and now that we have this let's just create a on key down function so const on key down and in here we're going to check if the user presses enter we're going to consider that they want to save that so go ahead and get the event again which is react.keyboard event with a type of html input element and we're going to check if event.key is enter in that case disable the input great so now that we have all of that let's go ahead and let's go revisit our input here let's give it a ref of input ref let's give it an on click to be enable input let's give it an on blur to be disable input let's give it an on change to be on change on key down to be on key down and the value is going to be our state title. Great. And now let go ahead and give this button an on click to be enable input Like that And that should be it Let go ahead and let see if this is working So right now, this file is called Untitled. Let's confirm that in the dashboard here. Okay, let's see. It's this one, I think. Let me actually just clear everything so it's easier. Clear the entire table from convex here. Great. Now we have an error. don't worry, we're going to solve that. So for now, just go ahead to slash documents. So remove the ID from your URL. And let's create a new page here. Let's go inside of it. And let's go ahead and let's rename. And there we go. It's renamed in real time because we're using a real time database. So our use query, which uses the get sidebar endpoint is also watching for the changes in real time. And we can do that for the children as well. So we can change this and you can see how it reflects even though it's a children. So it's a very, very cool database. Perfect. And you can see how this is changed in the database as well if you have any doubts. Perfect. So now what I want to do is I want to add a skeleton for this title so we can actually use some proper loading state in the navbar. So let's write title.skeleton to be function title skeleton and go ahead and return the actual skeleton which we have from add slash components UI skeleton. So just make sure you import that and let's go ahead and give this one a class name of H-9, W-16 and rounded medium. perfect now let's head back inside of our navbar component find where we have this if document is undefined we just return a paragraph and go ahead and change that instead we're going to render a navbar which is going to have the very same class name here except for the flex so we don't need flex you can just copy bg background dark px3 pay and you can just copy this actually we do need flex as well. So let's go ahead and copy the flex as well. My apologies. So just like this. BG background, dark background, padding on size 3, padding on top and bottom 2, W full like this and the flex and items center. Great. And inside what we're going to do is render title.skeleton like this. And now if you try and play around with this, you can see how for a second we have a nice skeleton instead of just a blank page here. You can see how it's a skeleton here. Perfect. And you can play around. You can reduce the size of the skeleton if you think it's too much. So let's see. Let's use height 6, for example. Maybe that's going to look a tiny bit better. Let's see. Yeah, maybe, maybe not. But, you know, you can play around and find the one you need. So let's see if I can reduce it to maybe 3 and maybe increase this. Will that do any better let's see uh maybe i go back to four so i'm just trying it out now uh let's see if that looks better yeah it looks like that one uh looks the closest maybe uh five would that be better let me just bring it back to six yeah okay i'm going to bring it back to what was it before was it nine It doesn't really matter. You can play around, but I just want it to look as good as possible. Okay, let's leave it like this. Great, great job. So you started with the Navbar and we do have a couple of more things to add to the Navbar. We're going to do that later. But what I want to do next is a little banner, which is going to show if you deleted a specific document. So if you go ahead and click it from here, you can still work with it, right? But I want to show a little banner here, which is saying that this is archived and give you the actions to unarchive it. So now let's go ahead and create that little banner here. So let's go back inside of the, let me just check the navigation a bit. I think I've noticed while I was watching the video back, I think I've noticed I made a mistake in one of the V, this W full classes. Perhaps it's here in the navbar. Let's see. W full here looks good. W full here looks good oh there we go I have a little bug here so this should be double full all right great so now let's go ahead and let's create that a banner component so that's why we added a little fragment here so we're gonna go below this right here and we're gonna write if the document is archived in that case we're gonna go ahead and render well let's not use the question mark let's use the end end option. In that case, we're going to render a banner component. And you can go ahead and save this and don't worry about the error for now. And let's go ahead and give it a prop of document ID to be document underscore ID, because that's the only thing that we need from it. And now let's go ahead inside of the components and create a new file banner dot dsx like that. Let's mark this as use client and let's go ahead and export const banner and return a div saying banner and go back to navbar right here and import the banner from dot slash banner like that and now let's go ahead and give it an interface so I can already see the banner here if you can't see it it's probably because you're looking in one of these so make sure you're in the trash or just add something to the trash and then click on it here. And then you should see this banner because we are only rendering the banner if the document is archived. Great. So now let's go inside of the banner and let's go ahead and create an interface. So interface banner props. It's going to accept a document ID, which is going to be a type of string like that. And you can just leave it. Actually, you can, let's give it the proper type. This is the proper type. Yeah. Like that. And now let's go ahead and extract the document ID. And let's assign that to banner props. And I just have a typo here. So ID like that. And there we go. Looks like this is okay, because underscore ID is this type right here, ID documents. Perfect. So we can give it that type in the props. Great. So now let's add our router. So constRouter is equal useRouter from next slash navigation. So make sure you add that. I'm just going to keep this import separated. Besides the router, we're also going to have our mutations for removing and restoring. So constRemo is going to be useMutation from convexReact. make sure you add this so we're going to have two mutations this one is going to be api so make sure you add api from convex generated api api.documents.remove and copy and paste this and this one is going to be restore and the api route is going to be restore like that great so now let's go ahead and let's import toast from the sonar package and let's go ahead and create some functions here. So const on remove, it's very simply going to be a function which gets the promise and calls the remove mutation and passes in the id to be document id which we have in the props. And then we can do toast.promise, pass in the promise and do the loading state which says deleting note. We're going to have the success state, which says note deleted. And we're going to have the error state, which says failed to delete note. Perfect. And now we can copy and paste this. And it's going to be very similar for the restore option. So this one is going to be called on restore. And instead of promise being removed, it's going to be restore. And instead of deleting is going to be restoring instead of note deleted is going to be note restored and this is going to be fail to restore note great so now we have these two actions on remove and on restore which call these two mutations here and now let's go ahead uh and yeah once let's also go back to this on remove here and once we remove it uh let's also add a redirect So .then here, let's just go ahead and write router.push to slash documents. Like that. Great. And now let's go and finally style this banner here. So go ahead and give this div a class name of W-full, background of rows-500. Great. Text is going to be centered. Text is also going to be small. Padding is going to be two. Text is going to be white. flex, items center, gap X2, and justify center. Great. And now let's go ahead and give this a paragraph, which is going to say this page is in the trash. Like that. And now go ahead and add a button here. And let's import the button from add slash components UI button. So make sure you have this import here. Great. And inside, let's just say restore page. And now let's go ahead and give it a size of small. Let's give it an on click to be on restore, which is the function we created. A variant is going to be outlined. And let's go ahead and give it a class name of border-white, dg-transparent, power, dg-primary slash five. text is going to be white let me just scroll down so this is centered for you on hover text is also going to be white padding one px2 height is going to be auto and font is going to be normal there we go and then you can copy and paste this button like this and make this one to be delete forever forever like this and use the function on remove like that but we're not gonna do just that I also want to import the confirm model so let's go ahead and import confirm model and remember this one doesn use the to stand instead it has a trigger and trigger is whatever we wrapped it around so in this case it going to be this button so confirm model like this and just wrap the entire delete forever button because this is an irreversible action so we want to make sure that the user has the confirmation for it if they accidentally click on it. And then you can remove this on click from here from the button. And instead, you can add it here and write on confirm to be on remove. Great. And let's go ahead and try this out. So now it says this page is in trash. If I click restore, it says note restored. Perfect. And if I go ahead and delete it again, now it says this page is in trash. And if I go ahead and check, it's right here. If I click delete forever, I have a model and confirm. And okay, it looks like it did not redirect us back. Let's confirm why that is happening, that we do something wrong here. All right, we do remove, then we do a then. Let me just see what's going on here. So let's try not by wrapping it in a dot then. let's just do it at the end of the function and remove this chain then here. Now we're still going to get an error because we are here. So just go back to slash documents like we did previously. And let's go ahead and attempt this again. So let me delete this. And let's go ahead and delete this forever. And there we go. Now we don't have the error. Great. So just go ahead and add that to the end of the function. No need to wait for this promise. So we actually want to be faster than the promise. Great. So now you have a fully working banner, which shows once you delete a specific page. And just one thing that I want to do before we go on to some other things that we're going to have in the navbar is I want to visit our components UI button dot TSX. And as you can see, it says that rounded is medium, I want to change it to rounded small like that. So I want every single button in our page to have a smaller roundness. And I just think it looks closer to original notion. Great. So we did that. And let me just go ahead and reset everything. So the next thing we're going to do is we're going to add a little menu here to also be able to delete the entire page from here. So currently, we can only do it from these three buttons. It'll be nice to also have those three buttons here at the top. So let's do that. Let's go back inside of our navbar component. So inside of the app folder, main components navbar. And besides the title right here, go ahead and add a div right here and give it a class name of flex, items-center and gap-x2. And go ahead and render the menu component, which we don't yet have. And give it a document ID to be document underscore ID. And save the file. And of course, we get a little error here. So let's go ahead and create that component, menu.tsx. Mark it as use client. And let's export const menu. And return a div saying menu. And now we can go back inside of the navbar and fix this error. So make sure it's not the lucid react import, but the dot slash menu. So from this very same folder, the same thing we did with the title and the banner. And now obviously we have some type script errors here. So let's go ahead and let's give it an interface menu props. And I'm just going to write the document ID to be a type of ID documents. Great. And let's go ahead and assign those props here. So menu props and let's extract the document ID. Great. So I just want to bring your attention. This is called menu and this from Lucid React I named menu icon. So if you didn't do that, if you have any errors with conflicting menu, that's because menu icon from Lucid React can also be imported as menu. But as you can see, then we have duplicate identifier here. So make sure that the one from Lucid React is menu icon. Or you can do menu as menu icon, for example. Just want to bring your attention if you're wondering, if you, for example, didn't do the icon thing and instead just named it menu. Great, so we have that now. And now we can head back inside of the menu here. And let's import everything we need from the dropdown menu. So go ahead and import from as slash components UI and a dropdown menu right here. We're going to need the dropdown menu itself. like we always do. And we're also going to need a trigger. We're also going to need the content. We're going to need the individual item and a separator. Great. And now let's go ahead and let's add some hooks and some functions here. So const router, oops, const router is use router from next slash navigation. Make sure you have that import. I'm just going to move it all the way to the top because I like to separate my global inputs from the ones starting with my alias. Besides the router, we're also going to extract the user from user, which we let from clerk, as I did right here. And you already know that I'm going to move it here to the top. All right. And now let's go ahead and let's also get the archive mutation. So use mutation from convex react. It's right here. Let's get it out to the top. And we also need our API. So make sure you import API from add convex generated API. And you already know API documents dot archive like that. And let's write const on archive. We are an arrow function where we are simply going to get the promise to be archive with an ID of document ID. and let's write toast from sonar. So just make sure you add this sonar import. So toast from sonar is going to be a promise. Passing that promise and let's give it a state of loading to be moving to trash. Let's give it a success state of note moved to trash and an error state to be failed to archive note. Great. And let's also do router.push to slash documents here once we archive it. Great. And now instead of this div, let's render the dropdown menu, which we imported. Let's add a dropdown menu trigger. Let's give it a prop as a child. And inside, let's add a button component. So make sure you import the button from ShadCN UI, I mean from components UI. All right. And let's give it a closing tag. And let's just simply write more horizontal from Lucid React. So we're going to render an icon from Lucid React. And now let's style this a little bit. So give this button a size of small and a variant of ghost. and give this one a class name of h4 and w-4. So we make this a bit smaller. Now add the drop-down menu content. And inside of here, let's go ahead and give it a class name of w-60. Let's give it an align on end, align offset of 8 and force amount. So let me just collapse all of this like this So it's easier for you to see. Great. And now inside of here, we're going to go ahead and render the drop down menu item. And we're going to render the trash icon from Lucid React. So make sure you import trash. Okay. We're going to give this trash a class name of H-4, W-4 and margin right of 2. And below that, we're going to add a delete text. and give this drop-down menu item an on-click to be on archive. Great. And now below this drop-down menu item, add a drop-down menu separator, which is going to have a div, which is going to say, last edited by user question mark pull name like this. And give this div a class name of text extra small, text muted foreground, and padding off too. Perfect. and now let's go ahead and see how that looks here. So now I have these three buttons here and there we go. It says delete, I can press on it and as you can see it's moved to trash and I can visit it back from here. Perfect. So now let's go ahead and let's turn this into a skeleton as well. So when this is loading, the same way we have a skeleton here, I want to have a skeleton on this side as well. So in order to do that, we're going to go inside of this menu component and add menu the skeleton to be a function menu skeleton and go ahead and return a skeleton component which I imported right here from components UI skeleton and go ahead and give it a class name of height 10 and W 10. Let's go ahead back inside of our navbar now and we have to find this loading state which we have and create a div below it with a class name of flex items dash center and gap x2 and add the menu dot skeleton inside like that we are also going to have another element inside of here so that's why we have this gap x2 the same reason why we have it here because we're going to have two elements here but we're going to skip that one for later. And now if you refresh everything, you should see for a second a little skeleton there as well. So let's just try it out. Oh, we are not seeing it. It seems to be right here. So let me see what we did wrong. I think I know what we did wrong. So let's just quickly see our loading state here Oh alright So I think it because in this navbar when we give it a flex we don give it a justify between option So let give it justify dash between So this title skeleton and this div is going to be far away from each other. So let's try that now. I'm going to refresh everything again. And there we go. Now for a second there, I saw a nice little skeleton. Perfect. So I have the skeleton here. I have it here. Exactly. what we wanted. Great. So now we're finally ready. And it seems like I have some weird error here. Let me just reload my window. Okay. And now we're finally ready to start developing the, well, the view for this document ID page. We're going to start by adding some title here and then a cover image and then the editor itself. And then what's left is to add a publish button. Great, great job so far. So now that we have our navbar semi-finished here, let's go ahead and let's head back inside of the document ID page so we can start rendering some content here. So find the document ID page. It's located inside of the app folder main routes documents document ID page dot TSX right here. And first thing I want to do is I want to mark this page as use client. So use client. And let's go ahead and let's import the API and the use query. So use query from convex react. And let's import the API from convex generated API. And then let's go ahead and let's write an interface document ID page props, which is going to have params which have the document ID inside of it which is a type of string but for ease of use we're going to write ID which we can import from convex generated data model and we're going to write documents so we don't have to change that every time we use it here and how do we know that we have this params and document ID where does this come from well it comes from this folder name, since this is a dynamic folder. And because you can think of this as a variable. So we created this folder, which becomes a part of the URL and the route. And whatever is written inside of parentheses becomes a variable, which we can access through params props. So every route has this page has these params, if it's visible. And there are different ways you can access params. So you can do it directly in the page like this, if it's that specific page, or you can use the use params, which we do in document list. For example, you can see that we use dot params here, and you can see that I have params dot document ID right here. So it's the same thing, but this way we don't have to use that hook. It would work if we use the hook as well, but I just want to show you different ways of doing that. Great. So let's go ahead and extract that params here and let's go ahead and give it the type of document ID page props. Great and now let's go ahead and let's fetch the document using the query. So const document is equal to use query api.documents.getById and let's go ahead and let's go ahead and give it the argument of ID, oh sorry, document ID to be params dot document ID. Great. And now let's go ahead and let's just write if the document is undefined. In that case, let's just go ahead and write a div loading. Later we're going to change it to a proper skeleton. And let's go ahead and let's write if document is no. Let's go ahead and let's return a div saying not found. And I forgot the return here. My apologies. So wrap this in a return. Great. And now let's go ahead and write the main function here. So let's go ahead and give this div a class name of padding bottom 40 like that. And let's go ahead and create a div here with a plus name of MD Max 3XL. So we are limiting how wide this document can go. And on large devices, let's do MD Max W for Excel. And let's write MX Auto. Actually, this is a mistake, sorry. So LG is Max W for Excel. Great. And now inside of here, let's go ahead and write document ID like that. And it seems like we're still not seeing it because it's behind this sidebar, but that's okay for now. We can ignore that. Instead, let's go ahead and let's replace this document ID with a toolbar like this. A toolbar doesn't exist yet. And all it's going to have is initial data to be our document, which we load. So now let's go ahead and let's create this toolbar component. So we're going to reuse this toolbar component. So we're going to create it inside of the components folder. So create a new file toolbar.tsx. Let's go ahead and mark it as use client. And let's export const toolbar and return a div saying toolbar. And go back to page.tsx. and then you can import toolbar from add slash components toolbar like I did right here. Great. Now let's go ahead and let's give it some props. So go ahead and write interface, toolbar props, and we're going to have the initial data, which is a type of doc from convex generated model, specifically the documents. And we're also going to have another prop, which is going to be optional and used later called preview. So it's going to be an optional Boolean. And let's go ahead now and let's just assign those. So toolbar props. And let's extract the initial data and the preview. Great. And now what I want to do is I want to go back inside of our page.tsx right here. and let's go ahead and let's just add a div here. It doesn't really matter. Let's just give it a self-closing tag and give it a class name of H35VH like this. So it gives a lot of space right here. Perfect. And now let's go ahead inside of this toolbar and let's go ahead and style it a bit. So I'm gonna give this toolbar a class name of padding left 54 pixels, group and relative, like that. And now let's go ahead and let's check if we have the icon. So if we have the icon, initial data dot icon, and if this is not a preview, meaning that we are looking to edit this, right? We are looking at this as an owner, not as a guest who is looking at our published document. If that is the case, let's go ahead and render a div like this with a class name of flex items that are center gap X two like that group slash icon and padding top of six like that. Perfect. Now let's go ahead and let's install a package called, let me just expand my screen. So let's install a package called emoji picker react. Go ahead and install that. So let's wait a second for this to finish. Great. And let's go ahead and let's create a new component called icon picker. So let's go inside of our components folder and create a new file icon-picker.dsx. And let's just save our work in the toolbar here. And let's go ahead and mark this as use client. Let's go ahead and import emoji picker. And let's go ahead and import theme from emoji picker react. Let's go ahead and import use theme from next themes, which we already have installed. And let's import everything we need from components UI popover. And that's going to be the popover itself, the popover content and the popover trigger. Great. Let's create an interface icon picker props, which is going to have an on change, which takes in the icon, which is a type of string because it's just going to be an emoji. Children are going to be react.react node. And as child is going to be an optional Boolean. Great. Now let's go ahead and assign that. So expert const icon picker. And let's assign the icon picker props. let's extract all of those props on change children and as child and for what i want to do now is get the resolved theme from used theme and let's go ahead and write const current theme to check for the resolved theme or give it default value of light as key of type of and what we have to do now is write theme map and let's go ahead and create theme map. So const theme map is an object which has an option for dark which is going to use theme.dark and it has an option for light which is theme.light. So I'm doing this because this emoji picker from emoji picker react has its own theme prop. So we have to use the theme that we are using using next themes and then map it correctly to what is compatible with their theme. And then let's just write const theme to be theme map current theme. All right, and now we can finally do return, get the popover write the popover trigger give it an option as child to be as child and inside render the children so that's going to be our trigger and then write popover content let's go ahead and end this like that and let's give this popover content a class name of padding is zero W fool border, none and shadow, none. And inside we're going to render the emoji picker.\nwhich is a self-closing tag. Let's go ahead and give it a height of 350. Let's give it a theme of theme. And let's give it an on emoji click to get the data and use the on change to send data dot emoji, which is a string like that. Perfect. So now we can head back inside of the toolbar right here. And now we can add that icon picker here. so import the icon picker from dot slash icon picker because both of those are in the global components folder. So we are working in toolbar and we just created the icon picker here and go ahead and pass inside of the icon picker a paragraph, which is going to be the initial data dot icon. Great. And now let's go ahead and give this a class name of text 6XL, cover opacity-75 and transition. And give this icon picker an on change to just be an empty arrow function for now, like that. And right now, you should not be able to see anything here. But let me show you a little trick to how you can preview something here. So no big deal if you don't do this. You can just continue coding as normal. But I wanna demonstrate that code picker to you. So right now, that emoji picker is only going to be shown if we already have an icon. So let's go ahead and see that. We can very easily do that by going inside of our database here. Go ahead. Let me just clear everything. So I'm going to go and delete this. And let me just delete this one as well like that. So let me go to this page and let me clear everything I have inside. Let's create a new one. And now all I have to do is give this document. If I can do that from here, I'm actually not sure. You know what? Ignore that. Let's just continue developing. I think I just confused you even more. Okay, I wanted to show you already, but we're just going to have to be patient. I'm sorry for that. All right, so now let's go ahead outside of this conditional right here. Actually, go outside of this icon picker here and add a button component. So import this from UI button. I'm going to change it to components. And go ahead and add a button and render the X icon from Lucid React. So make sure you add this import here. So this is going to be our button to remove the icon if we have it. And add a class name of H4NW-4. And let's go ahead and give this an on click for now to be an empty arrow function. And let's give it a class name of rounded-full opacity-0 group-hover-icon opacity-100 transition and text-needed-foreground. And we also need to give it a text of extra small. Besides class name, give it a variant of outline and a size of icon. Great. So I'm sorry you can't already see this. Perhaps I should have approached this in a different way, but it's quite hard to do it already. So let's just continue the way we are. I know you can't see this, but when we add an icon, you're going to be able to see this. So this is going to control the icon we can change or we can remove it altogether. Great. So now let's go ahead below that and let's write. Otherwise, if we have the initial data dot icon, but if this is a preview, meaning that a guest is looking at this, in that case, it's going to be much simpler. We're just going to render that icon. So add a paragraph and render the initial data dot icon inside. And let's give this paragraph a class name of text dash six Excel power. Well, it doesn't need any hover. it just needs a padding top of six. So this is gonna be if a guest is looking at it. So this is if we are looking at it, that means that we can change the icon and we can remove it, right? But a guest can just look at it. Great, so now let's go outside of this and let's create a div here with a class name of opacity-0 group-hover opacity-100 flex items-center gap-x-1 and padding y-4. And inside, if we don't have the icon, and if this is not a preview, in that case, go ahead and render the icon picker again. And this time, we're going to add a button component with a smile icon inside. So import smile from Lucid React the same way we did with the X icon. Let's go back down here. All right, and let's go ahead and write add icon and give this smile a class name of, it's gonna be H4W-4 and margin right to. Give this button a class name of text-muted-foreground and text extra small. give it a variant of outline and a size of small and give this icon picker as child option and on change is just going to be an empty arrow function perfect so now if you find it so just go ahead and hover over your screen or you can remove the opacity like this just change the opacity to 100 there we go and now you can click on this and there we go we have our emojis here so it's going to be right here the reason there's so much space here is because I just wanted to move it away from the navbar later on this is going to have a cover image so that's why we're not going to need all that space but for now you can look at this icon how it looks here and if you change the appearance to dark this is going to be dark as well so that's what I wanted to do great so now we have this add icon element here so let's quickly change this default opacity if you change it back to zero. Perfect. So now that we have that, let's go ahead outside of this. So still inside of this div, but outside of this conditional render here, and go ahead and write, if we don't have initial data cover image, and if this is not a preview, then go ahead and render a button again, which is just going to render the image icon from Lucid React. So make sure you import the image icon give this a class name of h-4 w-4 and margin right of two and give this button a class name of text dash muted dash foreground text dash extra small give it a variant of outline and size of small and for now give it an on click just an empty arrow function like that and if you try again and hover again we now have two buttons here but we forgot and to add a little text here below this image icon to be add hover perfect so now you can see when you just try and hover somewhere and there we go you now have add an icon and add a cover and if you're wondering why are they only showing when i hover on something well that's because we don't have the title rendered here, but we're going to have that soon and it's going to make a bit more sense. All right. So now that we have that, let's go ahead and let's add some states here. Let's go ahead and add some functions here. So first thing I'm going to add is const input ref to be use ref from react and give it a type of element ref, which has a type of text area with a default value of Now, then let's go ahead and let's add the state is editing and set is editing. We use state and false and make sure you import use state from react. So I have element ref, user ref and use state from react. And let me just move that to the top. All right. Besides that, we're also going to have the value and not import, sorry, constant value and set value from use state. And default is going to be initial data dot title. Then let's go ahead and let's write const update to be use mutation from convex react. So make sure you add use mutation from convex react. And we're going to call the API. So we have to import that as well, which I just did right here. So import API from convex generated API. I'll just move it with the other stuff here. and we're going to use api.documents.update. So we already created that function. Great. And now let's go ahead and let's write const enable input to be an empty arrow function. If it's preview, just break the function, right? Because a guest is looking at this. So a guest should not be able to even click on this. But just in case they do, we're just going to break the function. Let's do set is anything to be true. And let's do setTimeout to set the value of initialData.Title and to do inputRef.CurrentFocus like that and zero as the timeout value. Great. Now let's go ahead and let's add constDisableInput to just very simply be setIsEditing to false. Now let's go ahead and let's add const on input to take the value, which is a string, and do set value, value, and update ID, initial data underscore ID, and let's give it a title of value or untitled if we try and delete the entire title. So very similar to what we did with our navbar. Great. And let's add one more thing. const on key down to take the event, which is a type of react.keyboard event. And it's gonna come from HTML text area element. All right. And inside of it, what we're gonna do is check if event.key is enter And if that the case we going to prevent default And we going to disable the input Great. So now we can go back inside of our code here. Go all the way down where this ends. And go outside of this div. So right here, we should only have one closing div at the bottom. and show if it is editing and if it is not preview. In that case, go ahead and render a component which we actually have to install, which is called textareaautosize. So let's go inside of our terminal and let's install mpm install react textareaautosize. Like that. So that's gonna help us because, well, we could code our own, but this is just a faster way of doing it. So let's go ahead and import that here. So import text area auto size from React text area auto size. Perfect. And let's go ahead and use it here. So if we are editing and if it's not a preview, we're going to render that text area auto size and give it a ref of input ref. Go ahead and give it an on blur to be disabled input. Go ahead and use the on key down to be on key down. Go ahead and give it a value of value. On change is going to take the event and call the on input with event dot target dot value. And let's give it a class name of text-5xlbg-transparent font-bold break-words outline-none text-open pointy square brackets 3f3f3f. and on the dark mode, text is gonna be CFCFCF. And let's add a resize dash none so we don't have that icon. And now let's go ahead and write the else here. This is gonna be the last element here. And let's go ahead and write a div, which is gonna render the initial data.title. And let's go ahead and give this an on click. of enable input and let's give it a class name of padding bottom padding bottom to be 11.5 pixels text 5 pixels 5 excel sorry font-bold break words outline-none and we can copy this text and dark text from the text area out of size and just whoa i copied too much. So just this and paste it here like that and that should be it. Let's go ahead and take a look now. So there we go. We know you can see now it makes sense when we hover on our title that's when we have the options to add an icon or to add a cover and let's try and click on this and then you should be able to modify this and because we use the update function is here in real time. So now if you change it from the nav bar, it's right here or from here, it's visible in all three places. Amazing, amazing job. So now I want to go ahead and enable us to add an icon because currently when we do that, nothing happens. In order to do that, we have to create an API function for it. So we don't actually need an API route to add an icon, but we do need it to delete an icon. So let's get inside of convex inside of documents right here. Let's go all the way to the bottom and let's add export const remove icon to be a type of imitation. Let's get the arguments to be an ID of v.id documents. Let's get the handler to be an asynchronous function, which has the context and the arguments. And let's do the thing that we always do, which is get the identity and the user ID. So I just copied that from the function above. So const identity, we check if we don't have the identity and extract the user ID. Now let's get the existing document using await context.database.get, sorry, context.database.get args.id, like that. If there is no existing document, in that case, you can just throw new error, not found. And let's also do if existing document.userID is not identical to the user ID which we just extracted. In that case, throw new error unauthorized. And now let's go ahead and write const document to be await context.database.patch arguments.id and let's write icon undefined. So we remove that row and write return document here. Perfect. So now we can go back inside of the toolbar right here and let's write two functions here. So we're going to write const on icon select to get the icon, which is a string and call the update function using ID to be initial data underscore ID and an icon like this. So very simple. And now let's go ahead and do const on remove icon to be remove icon. Oops. Remove icon, and we don't have it. So let's go ahead and create it here. So const remove icon is use mutation API documents dot remove icon. All right. so we call the remove icon and we're just gonna go ahead and pass in the id to the initial data id as simple as that and now we can assign those two so find the icon picker here and go ahead and give it an option of one icon select for this button which holds the x class the x icon sorry give it an on icon remove or on remove icon sorry and we need the on icon select inside of this icon picker below as well like this and let's try it out now so if I click add an icon and select an icon there we go it's in all three places it's right here it's in the sidebar and it's right here and I can just as easily remove it or bring it back. And it's also in my database. Perfect. So we finished this heading here. And now we're going to go ahead and create our cover and upload our image. Great, great job. All right. So now that we have this toolbar and the icon working, let's go ahead and let's enable this add cover button right here. So before we go on to implementing the actual edge store, which we're going to use the upload the files. First, let's go ahead and create a model, which is going to open when we click on add cover. For that, let's revisit our hooks and let's create a new file use-cover-image.dsx. And let's go ahead and let's import create from to stand. Let's write the type cover image store. Let's give it is open to be a boolean on open to be a void function on close to be the very same void function and then let's do export const use cover image to be create and let's give this create a type of cover image store and let's extract set so just do double parenthesis inside set and open an immediate object like this wrapped in parentheses. And now let's assign is open to be false by default on open to call a set function which sets the is open to true. And on close, we'll call the set as well and set is open to be false. Great. And now let's go ahead and let's create our cover image model. So inside of components, models, and create cover image model.tsx. Let's mark it as use client. And let's import everything we need from s slash components UI dialog here. So we're going to need the dialog itself. Whoops. Dialog. We're going to need the dialog content and the dialog header. let's also import the use cover image from the hook we just created and now let's write export const cover image model to very simply use the cover image from use cover image and let's go ahead and let's return our dialogue so dialogue and let's give it a prop open to be cover image on open, sorry, is open. And let's give it an on open change to be cover image on close. Inside of the dialogue, add the dialogue content. Add the dialogue header. Let's see, did I import the dialogue header? I did. Okay. Add the dialogue header and add an h2 class, sorry, h2 element, which is going to say cover image. And give this H2 a class name of text-center, text-large, and font-semi-bold. And outside of the dialog header, let's just do a div which says to do upload image. Great. And now let's go ahead and let's add this entire dialog inside of our models provider. So let's go inside the providers model provider here And let go ahead and import cover image model And I just going to replace this to add slash components cover image model So models cover image model which we just created And let add it below right here Like that. And then we can go back inside of our toolbar component. So that's inside of components, if I'm not mistaken. Yes, it's right here. And let's go ahead and let's import it here. So I'm going to do it here const cover image to be use cover image from hooks use cover image so that hook we just created i imported it right here all right and now that we have the cover image let's go ahead and find the button which adds our cover image so i believe it should be right here this button which currently has an empty on click which says add cover and has the image icon so let's go ahead and give it a on click cover image on open. And now if you go ahead and click on add cover, there we go. You can see how we have a model and we have a to do upload image. Great. So this is a great start. Now we have our model, which is going to open to upload a new image. What we have to do now is set up Edge Store. So in order to set up Edge Store, we have to visit edgestore.dev or you can use my link in the description. So let's go ahead and click start for free right here. And you can create an account using GitHub. So let's go ahead and just sign in. And as you can see right here, I already have a project which I used for the initial development of the Notion tutorial. But for you, go ahead and find the button which says new project. And let's go ahead and name this Notion-tutorial. And let's click create. Let's wait a second for this to install. And there we go. Now you should be seeing these two keys right here. If this changes in the future, don't worry. I'm going to also show you some alternative ways where you can find your keys inside of your projects. But if you are seeing this screen, you can just go ahead and copy these keys and go inside of the dot environment file, which we have. So let me just close everything here. Find your dot environment dot local right here and just go ahead and paste the keys here. So you need to have Edge Store Access Key and Edge Store Secret Key. Great. And let me just quickly show you that you can always go inside of your projects. You can select a specific project. And from here, you can always click on this little info icon and then click on project keys and you can visit them here as well. So later when we actually have some files inside, this screen is going to disappear. But you can always see it by clicking on this little info here and then clicking on project keys. great so now let's once we've added this inside of our app let's click continue in docs right here and let's install everything we need so we need to install edge store server edge store react and zod and let's go ahead and click copy here and let's go inside of our terminal and i'm gonna go ahead and install all of that so i'm installing edge store server edge store react and zod and Let me just zoom out one more time just so you can see all of this in one line. So Edge Store slash server, Edge Store slash React and Zod and just go ahead and press enter. Great. Once this is installed, let's go ahead and see what the next steps are. So we need to add the environment variables, but we already did that. And now let's set up our backend. As you can see, it has both the instructions for the app folder and for the pages folder. we are working with the app folder so this pre-selected option works for us and you can just go ahead and click copy from here if for any reason you cannot find this you can always visit my source code and go ahead and find the file that I'm about to create now and we can take a look at where we need to create it so we need to create it inside of the app folder api edge store create the dynamic edge store route and then route.ds inside so let's go inside of the app folder create a new folder called API and inside create another folder called edge store like this and then inside create another folder in square brackets and spread edge store inside like this and create route.ts and just again copy everything from this file or copy it from my github and you can just go ahead and save this file. Great. Now that we have this, let's go ahead and let's see what the next step is. So the next step is to create a lib for the Edge Store, which is going to be our hook, which we are going to use in all our client components to easily upload files from the front end. So copy this file, make sure you're selecting the app folder, make sure and this is the front end section, right? And we need to create a lib called edgestore.ts. So let's go ahead inside of our lib folder, we already have utils inside. And now we're going to create a new file edgestore.ts and paste everything inside. Great. And now that we have that, we have to wrap our entire app inside of the edgestore provider. So let's go ahead inside of our app folder. so our root layout app folder layout right here and let's go ahead and let's import the edge store provider from at slash lib edge store so that file we just created and I'm gonna go ahead and wrap it inside of the convex client provider like this and just here where it ends just like that let's look at the documentation if that's it and looks like that is it we are now ready to actually upload a file. As you can see, we can do it very easily. We just need to upload Edge Store from use Edge Store. We need an input, which is actually going to allow the user to select the file. And then all we do is await Edge Store public files upload and we pass in the file. If we want to do some UI with the percentage of the progress that's been uploaded, we can use this hook for it right here. And if you look at the documentation even further, you can see that this Edge Store has a functionality to replace a URL. This is going to come useful with our cover image because we're going to have an option to replace an image. So it's very cool that we have this. We can just send the old URL that's going to be deleted and then we have the new one in that place. And you can also delete files very easily, but make sure that you also add the before delete lifecycle hook on the bucket. We're going to do all that. Let me just show you a bit more what you can do. So you can also do temporary files. If for any reason, your UI is needed for a temporary file, as you can see, they will be automatically deleted after 24 hours if they are not confirmed. And you have a nice little URL to confirm right here, which you get and then you can use that to confirm an upload. So just something if you're playing on using this in your own application. Great. So now that we have that, if you want to, you can already try with this very simple example. with an input, with a set file here, and then an upload. Or you can go ahead and set up this image component right here. As you can see, this is a drag and drop drop zone here. So I'm going to be using this single image component. So let's go ahead and skip the usage for now. Of course, you can take a look at it if you're wondering how this is going to look in the end. But what I want to go is I want to go to the installation part right here. and as you can see we have to install tailwind merge react drop zone and lucid react if i believe we already have lucid react installed as well as tailwind merge but it doesn't matter if you want you can just do npm install react drop zone or you can just copy this entire line but you will get some new versions with this if any new versions came out by the time you started this tutorial but i think it's safe enough to do that so let's go ahead and let's do this so npm install tailwind dash merge, react dash drop zone and lucid react. Go ahead and install all of these dependencies. Great. Let me just zoom back in. And now let's go ahead and copy the following component inside of our components folder. So it's right here, this very big component here. If for any reason, you cannot find this or maybe the documentation changed in the future, you can always just visit my GitHub and find this component, which I copied from here. You don't have to worry about it. And there are also much simpler ways of doing this. If you don't want the drop zone, you don't have to use one. But let's go ahead and click copy right here. And let's go inside of our components folder and create a new file single dash image dash drop zone dot TSX. Let me try and expand my sidebar. So single dash image dash drop zone dot tsx and I'm going to paste all of this inside. Great as you can see we have no errors meaning that we have all the packages installed. Perfect so first thing that I want to do is I want to go inside of this input props and I want to make the height and width optional like this so you can just put a little question mark here because we're not going to have them have fixed height and width because ours is in a model and then what I'm going to do is I'm going to go ahead and import spinner from dot slash spinner like this, because they are in the same component, sorry, in the same folder, the global components folder, we have the spinner right here and the single image right here. So we can just do dot slash spinner. Perfect. And now what I want to do is I want to go ahead and change the way it shows the loading state. So let's go ahead all the way down to where it actually returns something. So right here, and let's go ahead and give this main div right here a class name of relative. And let's go ahead and create an optional disabled. So if this entire single image drop zone is disabled, let's go ahead and give it a div, which is going to render our spinner component with the size of large, and go ahead and give it a class name of flex items dash center justify dash center like this absolute dash absolute like that and we're also going to have inset dash y dash zero height full width full and bg dash background slash 80 and z index of 50 like that perfect and now let go ahead even lower here and find this upload icon comment or this upload icon upload cloud icon if it easier like that And let's go ahead and modify this a little bit. So I'm gonna go ahead and remove this div, which has this button here. We're not gonna need that. And instead, I'm just gonna focus on this div right here. And I'm gonna change this text to be click or drag file to this area to upload like that. If you want, of course, you didn't have to change anything inside of this file, right? But I'm just doing exactly what I want so it looks better in our Notion clone. Perfect. So again, you can just visit my source code and copy the single image drop zone if you find this hard to follow. It's always a bit more complicated with working with code we have to copy, but that's completely fine. let's go ahead now back inside of our models cover image model right here and let's go ahead and import that component right so I'm gonna go ahead and import a single image drop zone and I'm gonna rename this to use add slash components like that and let's go ahead and let's create our state for the uploaded file. So const file set file is going to be use state from react. So make sure you add this, this import. And I'm going to go ahead and give this use state a file, sorry, a type of file like this. And let's go ahead and write const is submitting set is submitting to be use state false in the beginning like this and now let's go ahead and let's import whoops let's add the edge store from use edge store and we can get that from s slash lib edge store like right here as i did it perfect and it's going to be that easy to add our upload now now that we have our uh edge store sorry it's not edge store with a capital s it's edge store like this you can let's type safety completed for you. So I wrote it with a capital S. So that's why I had a little error. Make sure you just change it to a lowercase s. Great. And now let's go ahead and let's create our own change function. So const on change is going to be an asynchronous function, which accepts a file, which is an optional prop. If it has a file, it's going to go ahead and set is submitting to be true, it's going to set file to be file. And it's right const res to be await edge store dot public files dot upload that specific file as easy as that. And then let's go ahead and let's add our update mutation here. So I will just separate. Let me just do it like this. I'm just playing around so it makes more sense. Okay, so states are here and these other hooks are here. And now let's go ahead and let's add const update to be use mutation from convex react. So make sure you add use mutation from convex react. And let me move it to the top here and separate those imports. And let's add our API from convex generated API, API.documents.update. Great. And now inside when we have this response here, we can go ahead and write await update ID to be params, which we don't yet have. So let's do const params to be use params from next slash navigation. So make sure you add this input right here. And I'm just going to move it here. And we're going to use params.documentid as ID, which you can get from convex generated data model. So make sure you import this as well. And that's going to be a type of ID documents. and the thing we're going to update is the cover image and that's going to be response.url like that. Perfect. And now let's go ahead and let's add our onClose function. So const onClose is going to be an arrow function which set file to undefined. Set is submitting to false and call the cover image onClose. Like that. And then we're going to go ahead to the end of this await. And then we're going to call the on close like this. Perfect. And now we're ready to replace this to do and this div with our single image drop zone like this. And let's go ahead and give it a class name of w-full outline-none. Let's give it a disabled prop of is submitting. and let's give it a value of file if we selected it and on change to be on change. Great. So if we set up everything correctly, this should be working. So let's go ahead and prepare a couple of stuff. I want you to go inside of your dashboard in the Edge Store and go ahead and select your project. As you can see currently, I have no files inside, no back-ins, nothing. And now let's go ahead and refresh my project here, which I think I forgot to run or maybe because of too much changes, it shut down. So let me just find, there we go, looks like my project shut down. So if that happened to you, don't worry. It happens in development, especially when we do a lot of changes at once. So just go ahead and npm run dev. Make sure you have your npx convex running as well. And let's go ahead and refresh this now. Let's wait a second and there we go. It seems that the app is working. Let's click add a cover image and let's go ahead and select something. So I just changed the folder for my images and let's go ahead and I don't know, select this image from Unsplash. Let's wait and let's see if we have this. Let me refresh this right here and there we go. It says one file and we have the file uploaded right here. We currently cannot see it. So let's visit our dashboard, our Convex dashboard to see if it's stored in the database correctly. So go to convex.dev, go ahead and click on login and go ahead and select your project here. And there we go. Cover image, as you can see, has its own URL here, meaning that it's successfully uploaded. Perfect. And now what we have to do is, as you can see, we no longer have a button to upload a cover image because instead we should already be showing our cover image. So that's our next step. We're going to go ahead and render that cover image and add the buttons to replace it and to delete it. So let's go ahead and visit our document ID page. So I'm going to close everything here. Go inside of the app folder, main, routes, documents, document ID page.tsx. And let's go ahead and remove this div, which currently holds all this space. And instead, we're going to render our cover component, which currently doesn't exist. So when you save, you're going to get an error. So let's go ahead and add the cover, not inside of here, but inside of our main components, because that's going to be reusable as well. So add cover.tsx, mark it as use client. And let's export const cover and return a div saying cover image. Go back inside of the page.tsx where you added the cover and you can now import it from as slash components cover like this. And if you save, your errors should go away. Now let's go ahead and give this cover a URL prop to be document.coverImage. And let's go inside of cover right now and we have to create an interface for that. So interface power image props is going to have a URL, which is an optional string. And it's also going to have a preview optional Boolean. Let's go ahead and extract those props. So cover image props and extract the URL and the preview just like that. Now, you should no longer have any errors inside of this page.psx. And instead, we can focus on what's written here. so let's give this div a class name which is going to be dynamic so go ahead and import cn from lib utils and let's go ahead and let's first give it some default classes so that's going to be a relative it's going to be w full height of fixed 35 vh and group and now let's give it if we don't have the URL, we can lower that space. So let's give it a height of 12 VH. And if we have a new URL, we're going to go ahead and write VG to be muted as well. So for a second, it takes to load, it's going to take, it's going to look like a little skeleton there. Perfect. Now let's go inside and let's check if we have the URL in that case. So this is double exclamation point. Don't mistake it with a single exclamation point. It's a different thing. This will turn this into a boolean, which is right now a string or undefined. So we're working with booleans here. So if this is true, meaning if we have the URL, in that case, render an image component from next slash image. So just make sure you import this. I'm just going to separate those two imports. And let's go ahead and let's give it a source of URL itself. Let's give it a fill property, an alt of cover, and the class name of object-cover like this. And let's see if we can already see something and we can see an error, but this is a step in the good direction. As you can see, we have, if you watched my tutorials, a familiar error. So it says that the next image hostname files.edgestore.dev is not configured in our next config.js. So when working with images in Next.js, specifically when using the image component from next slash image, right? And if you just use the regular IMG element, this would not happen. But since you're using the optimized images from next slash image, we have to add the host name where our file is inside of next.config.js. So let's go ahead and look at this error. Go ahead and find where it says the host name and find the host name right here. For me, it's files.edgestore.dev. And we have to add that to next.config.js. So let's go ahead and do that. I'm going to go inside of my next.config.js, inside of this object, add images, domains, and go ahead and just paste that specific URL hostname inside. And my recommendation is that you also reset your servers.\nGo ahead and find your localhost here. Looks like it's working. You can see it found a change in next config, but let me shut it down just in case and rerun it because I found some weird errors are happening there. Perfect. And as always, let's go ahead and also do npxconvexdev again, just in case, you know. Well, I'm pretty sure this lives outside, separate of the Next.js environment, but I just want to bring your attention that we need to have this running as well in case you forgot about this terminal. So go ahead and rerun it just to refresh your memory of it. Great, and let's try and refresh now. And if I'm not mistaken, we should already be seeing our uploaded cover image for this document. There we go. Our beautiful image is right here. Perfect. And now we have to find a way to remove it and to replace it. So let's go ahead back inside of our component cover right here. And now let's go ahead and let's write if we have the URL and if this is not a preview, in that case, we're going to add some actions here. So this is going to be a div with a class name of opacity-0. But when we hover on this image, we're going to have the opacity to 100. it's going to be an absolute class with a bottom of five, right of five, a flex box with items centered and the gap between them of two. And let's go ahead and add a button. So make sure you import button like this. I'm going to change it to components. Great. And let's go ahead and write change cover. And let's give this an image icon from Lucid React. So make sure you import that. It's a self-closing tag and give it a class name of h-4, w-4 and margin write-off2. And let's give this button an on click for now to just be an empty arrow function. And let's go ahead and write class name, text-muted, dash foreground text, oops, foreground text dash extra small. Let's give it a variant of outline and let's just not misspell variant and give it a size of small. And then you can copy and paste this button like that and go ahead and change this to be X from Lucid React. So make sure you import X. and let's go ahead and let's write remove like this. And I think that should be it. Let's take a look now. When I hover here, there we go. I have the option to change the cover or to remove it. So let's go ahead and create these functions now. So to change the cover, we can just very simply add our cover image hook, right? So const cover image is use cover image. and we can do that from as slash hooks use cover image and then I can just do find this first button and give it an on click to be cover image dot on open like this and let's go ahead and try that so when I click cover image let's go ahead and select another image here let's wait for it to upload and there we go I successfully changed my image here and now let's go ahead and let's add the remove image from here. So we're going to have to create a function for that. And we're also going to have to create another API endpoint inside of our documents file. So let's go inside of convex documents right here. So very similarly to the way we created this remove icon, we're going to go ahead and create our remove cover image. So let's go ahead and write that export const remove cover image is going to be a mutation, which accepts the arguments to be ID of v.id documents. A handler is going to be asynchronous function, which takes the context and the arguments we pass from above. Let's go ahead and let's do exactly what we did above. So we get the identity user ID and let's also copy the existing document. So the identity, check for the identity user ID and existing document. If we don't have the existing document, in that case, throw new error not bound. If existing document user ID is not identical to user ID, throw new error unauthorized. And now let's go ahead and let's actually update the document. So const document await context.datamase.patch arguments.id cover image is undefined. Whoops. And let's do return document. Great. and now that we have that we can go back inside of our cover and let's go ahead and import the mutation so import use mutation from convex react and let's import our api from convex generated api and now let's go ahead and add the remove cover image from use mutation api.documents remove cover image. Great. And now let's go ahead and let's create const on remove to be an arrow function, which calls the remove cover image and just passes in the ID as params dot document. We don't have the params. So let's add const params to be use params from next slash navigation. Make sure you add this import. I'm going to move it here. And let's use params dot document document ID as ID from convex generated data model, because we need to use the proper type here, which is documents ID. Perfect. And now we can assign this on remove inside of this on click right here for the remove button. And let's see if that is working as well. So when I click remove right here, there we go. I now can do add a cover again. So let me just add a new cover here. again from Unsplash and let's see if it's still working. There we go, it's working and I can even expand my screen and you can see how it works as well. It scales and everything. I can collapse the entire screen and then it takes more space. Amazing, amazing job. Now let's go ahead and let's improve our change cover and remove functionality. So it works perfectly on the UI side But if we take a look at our Edge Store, you can see that inside I have three documents uploaded, but only one actual file that I'm using. So it will be nice if we had a cleanup function when we click remove and change cover replaces the old URL instead. So what I want to do now is I want to go ahead and clear my entire schema, sorry, my entire table from Convex database. and I'm gonna select all of the files here and I'm gonna go ahead and empty the bucket like this. And let me just confirm this. So Notion-Tutorial slash public files. And let's go ahead and empty these three files. Great, so now we have no files inside and let's go ahead and go back to slash documents. Don't worry about this error. We're gonna handle errors later. For now, just go back to slash documents inside of your URL. and let's go ahead and create a new page here and let's add a cover image here so I'm just going to use a random one let's wait for this to upload great so now we have a file here and if I refresh in the edge store I have one file here let's head inside of the documentation and let's see how exactly we can remove files so we're going to follow the documentation here we're also going do the replace file, but first let's do the delete file. So we can delete a file by passing the URL of the file inside the delete method. But in order to allow that, we need to set the before delete lifecycle hook on the bucket. So let's go ahead and click here and see what that's about. All right. So we have to find our Edge Store router. Let's do that first. I'm fairly certain that our Edge store router is located inside of API edge store route.ds right here. So there we go. It's edge store router right here. And let's see what lifecycle hook we actually have to give it. So here we have the before upload hook and we have the before before delete hook. It says that by default, every upload from our app is allowed. So we don't need to add this hook. I mean, we already confirmed that uploading works. Instead, we need to add this before delete hook right here. So let's go ahead and copy this. Let's remove this comma from here and let's paste it here. So we have this before delete. And in my case, we can just return true, right? We don't have to even extract any params from here. We can just simply return true. So make sure you add this inside of your EdgeStore router. And then let's go back and see what we have to do for deletion of the file. So we have to use await edgestore.publicfiles.delete and pass in the URL to delete. And we do that inside of our cover, right? Here we have the on remove button. So go inside of your cover component, which is inside of component cover.tsx. And let's go ahead and let's import edge store from, oops, use edge store from add slash lib edge store. Let me show you that. So add slash lib edge store. And I imported use edge store by destructuring it. And let's go ahead and inside of this on remove, First thing that I'm going to do is I'm going to write edgestore.publicfiles.delete like that. And I'm going to pass in the URL to be, let's see if we have the URL, we have it inside of the cover. So pass in the URL like this. And let's go ahead and let's wrap this inside of if we have the URL like this. Like this. And let's go ahead and mark this on remove to be an asynchronous function and put this inside of an await. So this only runs after this has been finished And let see if that going to improve our code and whether we did everything correctly So inside of our Edge Store right now I only have this one file And if I go ahead and remove the file, I should remove it from the database. That obviously happened. But let's refresh here to see if it removed from here. And it is. As you can see, the file is successfully removed from here as well. Perfect. And now what we have to do is we have to improve this functionality that when we change cover, it doesn't add a new image. Instead, it uses this replace file method right here. In order to do that, first thing I'm going to do is go back inside of my hooks used cover image right here. And I'm going to add a new method in the type to be on replace. And that's going to accept the URL, which is a string and give it a void. And then we're going to go ahead and add a, we actually have to add another one, which is URL to be a type of string. And let's make it optional like this. And let's now write on replace here to get that URL, which is a type of string. And let's go ahead and call set is open to be true and URL to be the URL, which we pass from here. and on close, let's add the URL to be undefined like that. Perfect. And you can also, if you want to add the URL undefined explicitly here, even though you saw that we got no errors, so you don't have to, but if you want to explicitly write it, you can do it here. Perfect. And now let's go ahead inside of our components cover right here. And when we call the discover image, we call the cover image on open. Instead, let's do this. Let's change this to be an arrow function, which calls cover image on replace and passes in the URL prop. Perfect. So now when we open this, we're going to open our model, cover image model right here. And in this cover image, we now going to have that URL. So let's go ahead and see how we have to do that. So I'm going to check if cover image has the URL. In that case, I'm going to run something else and I'm going to mark this in an else function like this. So let's go ahead and wrap this. And instead, let's go ahead and do it like this. Let's do res and change this to not be a constant, but to assign to that response and then do a res here. And let's go ahead and follow the replace file method. So we have to write await edge store that public files dot upload right here. let's go ahead and pass in the file we want to upload. And let's pass in the options to be replace target URL to be cover image, image.url like this. And it may be that we don't even need this if else. Perhaps we can just go ahead and add the options here. But first, let's try it this way. So I'm gonna go ahead and try that now. Let's go inside of here. Let's confirm that we only have one image here. and now let's attempt to change the cover here. I'm gonna go ahead and change it to another picture and if our code is correct, now there should still be only one but with a different URL. Let's refresh and there we go. Look at this, the URL has changed and we have a working image here. So our code is working and now let's try to improve it even further. As I said, perhaps we don't even need this if else. So let's go ahead and do this instead. I'm going to remove this. I'm going to bring this back to be a constant. And then I'm going to add options. And what was the code? I forgot. The code was replace target URL. So let's pass in the replace target URL and go ahead and pass in cover image dot URL. And it looks like it's working because the replace target URL can be undefined. So even if this is our first time adding an image, this code should work. Let's try out all of the cases now. So again, confirming your Edge Store, we only have one file. Let's go ahead and call the change cover and let's replace it with another file here. So I'm changing a completely different image. It's working. Let's refresh our Edge Store. There we go. The URL changed and we only have one file. Now let's try and removing the file. There we go. We no longer have this. Let's refresh Edge Store. no files here and let's try now adding a cover image to confirm that is working as well so something we haven't pasted let's see if that's gonna work and it looks like it's working when I refresh we have it right here perfect so we did not even need that if else clause we can just pass in the cover image dot url beautiful amazing job and now let's try and create a new title just to confirm that we did not mess up anything. So I'm going to go ahead and add another image from Unsplash here just to confirm that none of this are replacing the other image. So let me try and replace this child cover right here with some other image. Let's see this other image from Unsplash looks like it's working. This one did not change and I should only have two images inside and I do have two images. Let's try and remove this one from the child. There we go. This one is still intact and now I should only have one image. Perfect, looks like it's working as intended and we successfully have the deletion of our files. We have the replacement of our files and we confirm that individually they do not remove each other. So our code is working perfectly fine. Great, great job. So now that we have our fully working cover image here, let's go ahead and I just want to add one extra layer of security inside of this hook right here, use cover image. So I don't ever want us to accidentally replace an image. So whenever we use on open, I'm just going to go ahead and set the URL to undefined explicitly like this. So just in case we called the onReplace somewhere and somehow we forget to reset that, even though the onClose resets the URL to undefined, it would be weird that it's left in the state and causes some unexpected events. So I just changed onOpen to have URL undefined as well here. And now that shouldn't change absolutely anything. So I should be able to modify my screen right here. Let's see, I have one file here and when I refresh, I have another file instead and it's all still working. It seems perfect. Let's go ahead and let's confirm inside of another file here. When I add the initial image, I should now have two images in my Edge store. So let's refresh that that is working as well. That seems to be working. The last image was not misplaced. let's try and replace an image from the other child here just confirm that that function is still working so I always like to be thorough when making these changes there we go still two images and let's try and remove an image and now it should be only one image great everything seems to be working fine perfect now let's go ahead and let's go back inside of our cover.tsx so inside of the components folder. And let's go ahead all the way to the bottom and let's write cover.skeleton to be a function cover skeleton, which is going to return the skeleton from dot slash UI skeleton. And let's just go ahead and find where we import that. And I'm just going to change it to add slash components UI skeleton and move it here to the top. And let's go ahead and give this skeleton a class name of w-full and h of 12 vh like that and now let's go back inside of app main routes document id page.tsx right here then let's focus on this document undefined where we did the loading and let's now import the cover.skeleton here and let's add a div here with the class name of md max w 3 xl lg max w 4 xl mx dash auto and margin top of 10 and inside let's add a div which is going to have a class name of space dash y dash 4 padding left of 8 and padding top of 4 and let's add the skeleton component again from add slash components ui skeletons let me show you i just imported that right here and let's go ahead and give this skeleton a class name of height 14 and w 50 percent and you can copy this a couple of times and let's give this one these others a height of four like that and let's give this one 80 let's give this one 40 and this one uh 60 perfect so now we have a nicer loading skeleton here and let's try that out so i'm gonna make sure you refresh everything. And as you can see, when I go ahead and switch my pages, you can see how for a second I have a nice little loading skeleton, which looks just a bit better. Perfect. And now we're finally ready to create our editor. So let's go ahead and do that. So we have to head inside of our terminal here. And let's go ahead and let's do npm install at block note slash react and block note slash core like this. So these two items, let me try and expand this. There we go. So block note slash react and at block note slash core like this. And let's wait a second for all of this to install. And then we're going to go back inside of this page.tsx and we're going to add our editor finally. So let's just wait a second for this to finish. Great. And now let's go back inside of page.dsx where we just rendered this skeleton right here. And let's go below the toolbar and let's render the editor component. We currently don't have it. So if you save, you're going to get an error. Let's write on change for now to just be an empty arrow function. And let's write initial content to be document.content. like that. And now let go inside of the components folder where we have the cover the toolbar the single image drop zone right here and create a new file editor Let go ahead and let mark the entire editor as use client. And let's go ahead and let's import block node editor partial block from at block node slash core. Let's go ahead and let's import from at block node slash react. block note view and use block note like that. And let's go ahead and let's import the styles. So import at block note slash core slash style dot CSS like this. Perfect. And now let's go ahead and let's write the interface editor props to have the on change, which accepts the value, which is a string and a void. We're going to have the optional initial content, which is a string and editable to be an optional Boolean. And now let's go ahead and export const editor and let's assign this props here. So we're going to have the editor props and let's extract them. So on change, initial content and editable. And for now, let's just go ahead and return a div saying editor. Now we can go back inside of page.tsx and we can import editor from add slash components editor. So make sure you add this import and save the file and you should no longer have any errors. And now you should just see a text which says editor right here. Let's go inside of the editor here and let's start developing this. So I'm going to go ahead inside of the editor props here and I'm going to write const editor to be a type of block note editor which we imported and that's going to use block note. Let's go ahead and write editable here. Let's give it an initial content to be initial content, question mark, JSON.parse initial content. And we're going to give it a type as partial block and an array of it or undefined like this. So make sure this is written in one line like this. But let me just simplify it just so you can see it like this. All right. or you can just write it in one line as I did. I just don't want to keep this zoomed out so you can see better. And let's write on editor content change like this. And you can write it like that. You can make it as an object and then just give it a narrow function. And don't forget to add a comma here at the end. And let's do on change to use json.stringify like this, editor.topLevelBlocks null and two, just like this. Perfect. And now let's go ahead and let's go ahead and render the block note view, which is a self-closing tag. Let's give it an editor of editor. Let's give it a theme. Well, we have to carefully assign the theme because it's not the same as we, one, the one we use inside of our code. So let's write const resolved theme. We use theme from next themes. So make sure you import the use theme. I'm just going to move it to the top. All right. And now let's go ahead and let's write if resolved theme is dark, then we use dark, otherwise use light like this. And let's go ahead and see if we can already see this right here. As you can see, we have an editor now, I can do a slash, I can add a heading component, I can go ahead and add a subheading, I can go ahead and make a bullet list, you can go ahead and select this and bold it, italic, underlying, strike through it, you can add colors to it, You can make it nested. You can unnest it. You can create a link if you want to. You can also go ahead and move these blocks above another like this. So everything, you can almost everything you can do in the original Notion. And we are using this thanks to BlockNote. So you can always visit blocknote.js like this. And you can see everything it has in its documentation. You can also try it right here inside of this editor here, or you can visit the documentation and you can customize it. You can create different themes and styling. You can format the toolbar if you don't like it. You can modify the slash menu. You can even add additional blocks on your own. So a bunch of stuff you can do. And this entire block note editor is built on top of ProseMirror and TipTap, which are undoubtedly the best what you see is what you get editors in 2023. Perfect. So now that we have that, there's one thing that we have to do here. Currently, if we refresh right here, nothing that we've written is saved. And that's because we don't have the on change function. So let's go ahead and let's create that on change function by going inside of page where we render the editor. and let's go ahead and add const update to be use mutation from convex react so just make sure you add use mutation all right and we're going to import api documents dot update like this and let's add const on change to accept the content which is a type of string and go ahead and update the id using params.documentid and the content just like that and copy this on change and now we can go ahead and just simply assign it to here on change. Let's go ahead and see how that looks now. So now if I try hello world, if I add a heading hello big world and if I refresh the entire thing. As you can see, it's still here. Perfect. So you have a fully functioning editor here. But one thing that we are missing is when we select an image here, we can only embed by URL. But how do we add an upload file? Well, thanks to the combination of block editor and edge store, very, very simply. Let's go inside of editor right here. And let's go ahead and let's import useEdgeStore. So I'm going to go here and I'm going to import useEdgeStore from add slash lib useEdgeStore like this. Let's go ahead and let's extract the edge store from useEdgeStore. And now what we have to do is add a function const handle upload, which is a synchronous function, which takes in the file, which is a type of file. And all it's going to do is const response to be await edge store.publicfiles.upload and pass in that file like that and return response.url. Great. And now all we have to do is add that inside of the use block note. So we're going to do upload file to just be handle upload. Perfect. Let's go ahead and try it out now. So I'm going to go ahead and refresh this. All right. And let me delete this block and try again. So you can add the right image like this. And there we go. We can now click upload. And let's go ahead and do that. So let me just find my assets. And let's try and import something from here. As you can see, it's loading. And there we go. Look at our beautiful image from here. And if you take a look at the convex dashboard, you can see that we have content now. And this content is just a very big string of stringified object, right? Because it's too big to be defined as an object. And this is how it's officially recommended from the documentation of BlockNote to save it in the database. Perfect. Amazing job. You just added the editor here. Great. So what I want to do next is I want to go ahead and enable you to publish this note so everyone else can see it and also I think that if I currently go on to mobile mode right now it seems like I can change this cover and remove it but the problem is this is all on hover right so perhaps it would be a good idea to change it on mobile but maybe it even looks okay I don't know you can play around with it yourself but what I'm gonna focus on now is go ahead and publishing this. But before we do that, let's take a look at everything we left commented out. So currently, when I create a new page, I'm not redirected to that page. Let's change that. So I'm going to go back inside of my components folder navigation. Where is it? It's app main components navigation right here. And I think I commented out this function. Let's try and find it. Is it even here? Maybe it's not here. Let's try and find this. So I need to find this new page, which uses handle create. Where is it? Oh, it's right here, handle create. Okay, and what I want to do is write dot then. And what we need to do is redirect to that page. So we can just simply get the document ID from this callback and then do router dot push. And we don't have the router. So let's go ahead and add the router here. so const router use router from next slash navigation so let's see where i imported that there we go use router from next slash navigation and then let's go ahead and find the handle create again and let's use the lowercase router.push let's use the back text slash documents slash individual document id great and now if i go ahead and click new page here there we go i'm redirected to it. And if I click here, I'm redirected to it as well. So let's just confirm. There we go. One place where I think this is not happening right now is if I go ahead and let's delete this forever. If I click from here, I'm not redirected into it. So let's go ahead and resolve that. So I'm going to close everything. I'm going to go inside of the app folder main routes. I'm we going to go inside of documents page And in here the very same thing we have the onCreate So let go ahead and add Let get the document ID And let go ahead and add our router Use router from next slash navigation. Let me just move it to the top with the others. And let's do router.push backtick slash documents and individual document ID. Great. So now if I click from here, it should be the same thing. Perfect. And let's see some inconsistencies here. So when I delete from here, I'm redirected to this screen. But if I delete it from here, I'm not redirected. So you can choose the functionality you want, right? It doesn't matter. But what I want to do is I want to make it equal. I want to make it if I delete it from here, I also want to get redirected away from it. So let's find that. I think that's located inside of the app. main components item right here. I think we have a function. Oh, great. So we have the on create here as well. So we have to fix that. But let's first find the on archive like this. So once we finish this, let's go ahead and let's do router.push to slash documents like this. So when I delete it from here, I also want to get redirected. So pick a random document, make sure you select it and delete it from here. And there we go. We are redirected to here. Okay. And now what I want to do is that when we click create on here, I'm redirected to the child document. That would make sense, right? So let's go ahead and find the onCreate inside of item component. And I commented out this router.push because at the time we didn't have it. So let's go ahead and see how that works now. When I click plus here, as you can see, the child is immediately the new selected component. Perfect. Great job. So what I want to do now is just go ahead and quickly fix how we're supposed to import the editor component. So let's go ahead inside of editor component. So components editor right here. And I actually don't want to do export const editor. Instead, what I want to do is just const editor. and then at the bottom, I'm gonna do export default editor. So now what I can do is go back inside of my app folder main routes, documentidpage.dsx where I use the editor and I'm not going to use this import right here. Instead, what I'm gonna do is I'm going to import dynamic from next slash dynamic and I'm also going to import use memo from React. So let's go ahead inside of our document ID page right here and let's write const editor to be use memo, open an arrow function and just add a dependency array at the end. In this arrow function, call dynamic and that arrow function inside is going to have an import which is going to call the s slash components slash the editor like that. But we're not done here. we also have to pass in this option, SSR false to it. So let me try and zoom out so you can see this in one line here. So we use use memo, we call an arrow function, which calls dynamic, which opens parentheses and calls its own import. And then inside of the dynamic parentheses, we set the configuration to not use server-side rendering on it. So this is the recommended way from the block note documentation. Great. So now that we have that, I think we are finally ready to handle our error state. That's one thing that you've probably noticed we have not handled yet. So I'm talking about this. Let's go ahead inside of a trash and let's copy the URL for this one. And let's click delete forever and remove it. And if I go back to it, what happens is that we get an error. Well, that's expected, but I think we should handle it in a more graceful way. so I want you to go inside of my GitHub and go ahead and find the public folder and you're going to need to download the error dark and the error.png files so let's go ahead and prepare our public folder so here on the side I'm just going to close everything and I'm going to open my public folder and let's just expand this a bit so find the error.png and go ahead and download that file So I'll drag and drop it inside of my public folder. And now go ahead and download error dark. So go ahead and download that file as well and drag and drop it inside of your public folder. Now just go ahead and confirm that both of these files are named correctly. So it should be error.png and error-dark.png. And now let's go ahead and close everything and let's go inside of our app folder. And here in the root, create error.psx. Let's go ahead and mark it as use client because every error component needs to be a use client component. Let's import image from next image. And let's import link from next link. And let's import the button from add slash components UI button. And then we can write SFC error like this. and let's go ahead and do a div here with a class name h-full flex flex dash call items dash center justify dash center and space dash y dash four and inside go ahead and render the image so we're going to go ahead and render source error.png we're going to give it a height of 300 a width of 300, an alt of error like this. And let's also give it a class name of dark, hidden. And then we can go ahead and copy and paste this image component. And you can give this one an error slash dark and give this an error block, but hidden on default. Great. And now let's go below these two images, another H2 element, which will say something went wrong. And let's write class name, text dash extra large and font dash medium. And let's add the button component. Let's give it an as child prop. And inside of it, let's render a link component, which says go back and give it an href of slash documents like this. And make sure you do export default error. And there we go. Now our error screen is looking much better. So in development, we can still see the errors which are happening. But in production, users will not be able to see this. But they will be able to see this screen right here, which when we go back, leads to the working slash documents page. Great. Amazing job. You're so close to being done here. One more thing that I want to do now is just enable you to publish a specific page and then allow guests to view that page. Perfect. Amazing job. And after that, we're also going to do the deployment, of course. So now let's go ahead and let's create the publish functionality. But before we do that, I just want to quickly bring your attention that my icons in the navbar are not exactly aligned as they should be. You can see that this little button here doesn't exactly match up to this button here. Same with this icon. It just seems a bit off and I just caught that. So let's go ahead inside of the app folder, inside of main components and pick the item.tsx right here. And go ahead and find where we have this document icon if else clause. And you can leave this one as it is. But inside of this icon, besides the height 18 pixels, also give it a width of 18 pixels like this and save the file. and now as you can see it's much more aligned one below another and it looks a bit better it was bugging me I know it's a very small detail but you know that's what we do in this channel we focus on the details great so let's go ahead now and let's create the publish button which is going to be right here next to my drop down for the deleting a specific document so I'm going to go ahead inside of the navbar. So you can find that in app main components navbar.tsx right here. And go ahead and find where you render the menu right here. And above it, go ahead and render the publish component, which we don't yet have. So you can save and you're going to get an error. And let's go ahead and write initial data to be the document, which we have inside. Now let's go and create that component. So go ahead and create publish.tsx inside of underscore components in the main folder here. Let's go ahead and mark this as use client, export const publish to be an arrow function, which just returns a div saying publish. Great. And now we can go back to the nav bar and you can import that component from dot slash publish the same way you did with title, banner, menu. And now we do with publish. Great. And let's go ahead and let's just refresh this to confirm nothing is going wrong anymore. There we go. We have a big publish text right here. And now let's go ahead inside of the publish and let's fix this TypeScript error, which is telling us that we have the invalid props for it. So I'm going to go ahead and create an interface publish props to have the initial data, which is a type of document from convex generated module, specifically documents schema. And now let's go ahead and assign that. So publish props. And let's go inside of here and write the initial data. Great. And now I want to create a hook called use origin because we're going to need it for this part. So let's go inside of our hooks and create a new one called use origin.tsx. Let's go ahead and import use effect. And let's import use state from react. and let's write export const use origin. Let's do mounted, set mounted, use state false because we're gonna use the window object inside of here and that can cause hydration errors when it comes to server-side rendering. And now let's write\norigin is type of window not undefined inside of apostrophes like this. So we're checking if type of window is not undefined. In that case, we're going to do window.location.origin? window.location.origin. Otherwise, we're going to use an empty string. And now let me just zoom out so you can see how this looks in one line. So origin type of window is undefined, is not undefined. In that case, we're checking if we have the window location origin. If we do, we are using the origin. Otherwise, we are adding the empty string. Great. And now let's do use effect here. Set mounted to be true and give it an empty dependency array. If we are not mounted, just return an empty string. Otherwise, you can return origin. Great. And now let's go back inside of publish.tsx and let's import everything we need from our popover. So import from popover at slash components UI popover. Let's import the popover trigger. Let's import the popover and the popover content. Great. And now let's go ahead and let's write some functions and some states which we're going to need for this. So first, let's get the origin from use origin, which we just created, which is at slash hooks use origin. So make sure you add this import here. Now let's go ahead and let's add our update mutation. So const update is use mutation from convex react. I'm just going to move it to the top here. And we are also going to need the API. So import the API from convex generated API. And it's going to be API.documents.update. And now let's add some states. So we're going to write const copied, set copied, use state from React false. So just make sure you import use state from React. And I'm going to move it to the top here. Besides this one, we're also going to have is submitting and set is submitting. to be use state false. Let's get the URL to be origin slash preview and initial data underscore ID. So using this constant, we are generating what is the URL that the user can share with guests to look at the published note we have. So that's why we needed the origin hook so we can get whatever this website is being hosted on. So if it's local host, is going to say localhost. If it's whatever, http something.com, it's going to be http something.com slash preview slash this. Great. And now let's add the function to actually publish this. So const on publish is an arrow function which does set is submitting to be true. And then const promise is an update function which has the ID of initial data underscore ID and is published is set to true. And now let's go ahead and do that finally on this function. And let's just do set is submitting to be false. And now let's give it a toast here from Sonner. So make sure you import toast from Sonner. I'm just going to move it here. And inside of it here, we're going to do toast dot promise, we're going to pass in the promise. And we're going to write the loading state to be publishing. We're going to get the success state to be note published. And we're going to write the error state to be failed to publish note. Great. And now we can copy and paste this entire on publish function. And we can rename it to on unpublish. and we're going to do the is published here to be false like this and we're going to change the toast to be unpublishing and this is going to say note unpublished and this will be failed to unpublish note. Great and one more thing that we have is const on copy so we can copy the published URL that's going to be navigator.clipboard.writeTextURL which we generated in this constant right here and then we're just going to say set copied to be true so we're going to show a little icon with a check mark if we successfully copied it and then we're going to wait out one second before we change the icon back to what it was originally so a thousand milliseconds is one second and we're just going to set copy to be false here great and now let's go ahead and let's render our popover component and inside of here add a popover trigger which has an as child prop and inside of it we're going to render a button component so make sure you import button from add slash components ui button let's go ahead and write an end tag here and inside we're going to render publish like this and we're going to check if by chance we already published this so if it's published initial data that is published in that case we're going to go ahead and render the globe icon from lucid react so make sure you import globe from lucid react i'm going to move it here to the top so globe and it's a self-closing tag like this let me just wrap this in parenthesis so I can do it in multiple lines. And we're gonna go ahead and give this globe a class name of text sky 500, W of four and height of four and ML two like this. And let's go ahead back to this button now, give it the size of small and a variant of ghost. Great. And now let's go ahead outside of the trigger here and let's add the popover content. Let's give this popover content a class name of width of 72. Let's give it an align of end. Let me just collapse these items here. So align of end. And let also do align offset to be eight and force mount Now inside of here check if we already published this So if initial data is published, in that case, let's go ahead and create a div here for now, which is just going to say published. Otherwise, let's go ahead and create the screen, which we're going to see first, which is going to be a div with a class name of flex flex dash call items dash center and justify dash center and inside of this div let's go ahead and render the globe which we have imported and let's go ahead and give it a class name of h dash 8 w dash 8 text muted foreground and the margin bottom of two. Let's write a paragraph here which says publish this note and let's write a class name of text small, on medium and margin bottom of two. And below the paragraph, let's add a span element which is saying share your work with others. And let's give this a class name of text extra small, text muted foreground and also margin bottom of 4 and below that add a button which will say publish and this button will be disabled if we are submitting like that it's going to have an on click to be on publish and the class name is going to be pulled with a extra small text and size is going to be small and i think we can already see this now so let's go ahead and click on this publish here and there we go we have a pop-up which says publish this note and share your work with others and if you click publish there we go now it should just say published and if you check your convex database so let me go ahead and log in here and let's go ahead and find that is published, you can see that one of my documents right here is published. So this is true for it. Perfect. So it's working. And now let's go ahead and you can see how I have a little icon here because it's published. And now let's modify how this looks when it's actually published. So we need to have the button to unpublish it. And we also need to have an input where the URL is going to be written. So give this div a class name of space Y4. Create another div inside with a class name of flex, item center and gap X2. Add a globe icon in here with a class name of text sky 500, animated dash pulse and height of four, sorry, pulse, height of four and width of four. go ahead and add a paragraph this note is live on web give this a class name of text extra small on dash medium and text sky 500 and outside of this div create a new div with a class name flex items center all right and inside of here open a native html input like this is going to be a self-closing tag give it a value of the url which we have as a constant and give it a class name of flex-1 px-2 text extra small border rounded dash l dash md so only on the left side is going to be rounded height of 8 and bg muted and truncate and important it's also going to be disabled and now right beside it we're going to add a button which is going to check if we have copied it in that case it's going to render the check icon from lucid react so just make sure you import the check icon from lucid react and this is going to have a class name of height for and width for like that and otherwise we're going to render the copy icon from lucid react so make sure you add the copy icon as well and give this a class name of height 4 and width of 4 like that and give this button an on click to be on copy give it a disabled prop to be copied and give it a class name of height 8 bg sorry and rounded dash l dash none like that perfect so let's take a look at how this looks let me just refresh this and there we go you can see how now i have an input and i can copy this now and if i try and paste this you can see that i have that copied preview url you if you try and go there, you're going to get a 404. We're going to do that in a moment. And now we need to add a button here to unpublish the note if we want to do that. So let's go ahead and let's go outside of this div. And let's just add a button here, which is going to say unpublish. And let's go ahead and give it a size of small. Let's go ahead and give it a class name of W full and text extra small. Let's give it a disabled prop of is submitting and let's give it an on click to be on unpublished. Great and now I think that should be it. So let's go ahead and click publish here. If we unpublish you can see that note is unpublished and it's no longer available. But when I click on publish, you can see that the note is live on the web and we can copy that URL link. Perfect. So now we actually have to create this route because if I go there now, we're getting a 404 because the preview page doesn't exist. So let's go ahead and do that. So we can actually do that quite easily because we have all the components we already need there. So let's go ahead and create a new app organizational router, sorry, organizational folder, which is going to be called public. So this is not going to have any of the layouts that we usually have, like a sidebar or an off bar, right? So this is why organizational routes are so cool. And now go ahead inside of here and create another folder routes And inside of it create a new folder preview And then the final folder which is going to be the individual document ID inside And then create a page And what you can do is just go inside of main routes document ID page and literally copy everything inside. So this page, which renders the editor using use memo and dynamic and has the params, you can copy that entire page, go inside of public and paste it inside of this page right here. and as you can see we have no errors with the imports because these ones are reusable right so we have no problem to that but we do have to change a couple of stuff so first thing that i want to do is i want to give this cover a preview prop so it's not going to be editable same thing for the toolbar a preview prop and same thing for the editor here a preview prop sorry not preview but editable is going to be false like that. Perfect. And now let's go ahead and try if this is already working. So I'm going to click publish. I'm going to copy this. I'm going to paste it inside. And what I should see is this very same document, but without the sidebar, I cannot edit. I cannot add anything. I cannot remove the image. Nothing. Perfect. So this is exactly what we want. And now let's just try it out. if this works when we are logged out. So I'm gonna go ahead and copy this here and paste it in incognito mode where I'm logged out. And there we go. Look at it. It's working, right? Perfect. And now let's go ahead and go back here and let's unpublish it from here. So I'm gonna click unpublish and look at this. Now we have an error because it's no longer working. And I just wanna bring your attention that technically you can still go here where you're logged in and here I think you should oh maybe not no okay no worries I thought maybe you could still see it from here but not great so now you can publish the route you can refresh from here and everything seems to be working but it looks like we have a little mismatch of colors in our dark mode so we're gonna fix that of course but that is it we've finished all the functionalities we need for this project amazing amazing job all that it's left is to actually deploy this entire application. But before we do that, let's just go ahead and go inside of our dark mode and see if anything needs fixing. So it looks like here, everything is okay, but it looks like on preview, this background is too dark. Let's take a look at this publish button. This looks fine. This looks okay. This looks fine as well. The trash, we already fixed some stuff. That looks all good. Let's go ahead and, well, everything actually seems fine. It looks like. Great. So let's go ahead in the preview and make sure this is published. So let's just refresh this. And let's go ahead and fix the color. And we can do that easily by going inside of the preview right here. So inside of the public folder and just create a file layout.tsx like this. So we reflect everything that's inside. And let's go ahead and let's write SFC public layout. Let's go ahead and let's extract the children from it like this. Children react dot react node. And now let's go ahead and let's render a div with those children. And let's give this div a class name of age full and dark background to be a hex code. One F, one F, one F. Like this. Perfect. Let's try it out now. So I'm going to go back here and there we go. Looks beautiful exactly as we expect it to look like in light and dark mode. Amazing, amazing job. This was a very hard project. We learned a bunch of different technologies in this project and you completed the entire thing. And all that's left to do is to deploy it. let's go ahead and do the last step of this tutorial which is deploying the application so if you haven't already go ahead and create a repository for this application i did this in the beginning of the tutorial when i explained you how to use trunk but just in case you haven't done this let's go ahead and see how we do this so go ahead and open a new repository on github and go ahead and give your repository a name like next 13 notion tutorial or actually I'm going to use Jochen tutorial like this and I'm going to set it to private and I'm going to click create a repository and after this is done since this is not a new new repository right we have a lot of files here you're going to select this second option push an existing repository from the command line and let me see if I can do that because I already have a repository inside of this. So let's see if I can do this. I'm going to go ahead. And what you can actually do is shut down everything. So you're no longer going to need the convex dev or the npm run dev. And just paste these commands from here. And there we go. I have an error because remote origin already exists. But let's see if this command still worked or not. That refresh looks like it's not working. But for you, it's going to work if you haven't set up your repository beforehand. On the other side, if you already have the GitHub repository. So just refresh this page and then you're going to see your GitHub repository. So I already have it. Let me just show you where it is. So let's go inside of my repositories and I have it right here, Notion-CloneTutorial. So this is my repository. And now let's take a look at everything we need to do to prepare Convex for production. So click on Docs right here and find the Production tab inside and you have the option hosting and employment and select Vercel and let's go ahead and see what we need to do here so we need to create a Vercel account so do that if you haven't and try and sign up with your github since it's going to be easier now we have to link our project on Vercel so click add new here and select a project and go ahead and select that repository there is for my case, it's notion dash clone dash tutorial, go ahead and click import here. And I'm just going to change this to something else because I recommend you don't use the original application name inside of your URL, because it can be labeled as a phishing site. So I'm going to change this to note taking app like this, you can leave the framework present as it is. And let take a look at what we need to do next So it says we need to override the build command So let go ahead and do that Click on build and output seconds and click on the override tab right here and change the run command to be npm run build and convex deploy like this. So npm run build and convex deploy and let's see what was previously it was next build. So now it's npm run build and convex deploy. And now we need to set up our environment variables. So first let's do it the easy way. Go inside of your dot environment dot local, go ahead and copy everything from here and paste it in this one. And there we go. I have all of my variables right here. But we're not done here because we have to change our convex project to production. So let's go inside of our convex dashboard, select the application you're working with. And let's go ahead and choose production like this. And there we go. Now it's creating our production. And as you can see, this is completely empty. So let's try and find our URLs here. So I clicked on settings right here. And let's see what we need to do. So what we need is the deployment URL right here. So let's go ahead and copy this deployment URL. Let's go here and let's find that next Publix convex URL. I think that is this one. So let's paste it here. So replace it like that. Now we have the convex deployment here. And let's see if there's anything else we have to change. Let's try and generating a new deployment key like this. There we go. So deploy key right here. Go ahead and copy that and change the convex deployment to be the production instance of it like this. And I think that should be it. So in this case, it's convex deploy key, but since we're working with Next.js, I think ours is correct. If it's not, no worries. And if we have any errors, we're going to fix them. So let's click deploy. Make sure you did these changes for production. And I'm going to pause the video and I'm going to show you the results I have from the deployment. So it looks like I have an error here and I'm pretty sure you're going to have it as well. The deployment failed and it says that convex deploy key is not set. So it looks like we made a mistake following this after all. So let's go ahead and leave this as it is. And I'm just going to zoom out and I'm going to click back here and let me just go ahead back inside of my projects here let me refresh this so I have this note-taking app which I tried to deploy but it failed I'm going to click on settings here and I'm going to go on the environment variables here on the side right and let's go ahead and see so we have the convex deployment but we don't have the convex deployment key. So let's go ahead and create that. So copy this deploy key from here and we have to name it convex underscore deploy underscore key. So let's add that. So convex underscore deploy underscore key and paste it like this and click save. There we go. So now we have the convex deployment key like this and I'm just going to leave this as it is. I'm not even sure if we need convex deployment after all. And now that you added the new environment variables, go back inside of deployments here, find the one that failed, click on these three dots here and click redeploy and click redeploy one more time. And I'm going to go ahead and pause the video again. And we're going to see if there are any more errors we need to fix. And there we go. Looks like it's working. So now let's go ahead and click on note taking app here, click on project so you can get the url which is notetakingapp-rose.varsal.app in my case in your case it's going to be something different and look at this oh seems like we have a little bug here oh well we're going to fix that right away so i'm going to show you how to fix it but let's go ahead and check if everything is working so i'm going to log in with my github from here i'm going to authorize clerk let's go ahead and there we go i can enter joshan and since we are in production we have no pages created here. And there we go. So I can start from the beginning here. Perfect. Real time is working. Let's test our upload if that is working as well. So let me just find my folder. All right, let's select a random image from here. Let's see if that is going to work. There we go. That is working as well. Let's try it from here. So I'm going to upload an image from here as well. Let's go ahead and add that. There we go. Let's try and remove an image that seems to be working. Let's try and deleting this. It's in the trash. We can recover it. Everything is working as we expected. Let's add a little icon here. There we go. Everything in our project is working. The only problem we seem to have is in our landing page. So let's try and resolve that. So I'm going to go ahead and log out. Just make sure you have your app running again. So let me just go ahead and do terminal here, npm run dev inside of my project. And let me just refresh this. So I should be back on my landing page. And there we go. This is an error we have. Well, not an error, but it looks like our background is not fully dark here. So let's see how we can resolve that. first thing I'm gonna check is the layout page of the marketing folder so I'm gonna go inside of my app folder marketing here and I see I have a layout page and it looks like in here we do give it a specific dark color so let's see what else could we change here let's go inside the page.tsx to see that maybe here something is missing. And yes, let's try and give that dark background inside of the page.tsx as well like this. Let's see if that fixes it. And that seems to have fixed it. So how do you redeploy? Well, very easily with Vercel. All you have to do is add this change, commit this change, so fix and git push like this. Go ahead and push that and now you can go back inside of your Vercel. You can click on deployments and as you can see I have a new deployment building right now. So I'm just going to go ahead and pause my screen and then we're going to visit the landing page again. All right, it's successfully deployed and let's go ahead and check it out now. So I'm going to go ahead and log out. And let's take a look and there we go. We no longer have that black bar here which we had. Everything is in the color it's supposed to be. Perfect. Amazing, amazing job. Thank you so much for watching this entire tutorial. And remember to leave a like, share, comment and subscribe and see you in the next one.",
  "transcript_chars": 390526,
  "transcript_filled_at": "2026-06-06T15:37:40.484945+00:00",
  "transcript_filled_by": "tk-bulk-groq-retry-20260606"
}