{
  "video_id": "gq2bbDmSokU",
  "title": "Build a Real-Time Google Docs Clone With Next 15, React, Tailwind (2024)",
  "url": "https://www.youtube.com/watch?v=gq2bbDmSokU",
  "transcript": "Hey there! My name is Antonio and in this course I'm going to show you how to build a fully functional Google Docs clone using modern web technologies. As you can see, multiple users can work on the same document simultaneously. While one person is typing, another can format text, change colors, or even add comments. You'll learn how to implement a commenting system where users can tag each other and discuss specific parts of the document. These interactions don't just stay in the document, we've built a notification system that keeps track of all mentions and comments. When creating new documents, users will be able to choose from various templates. We are going to implement several options like software project proposals, business letters and resumes to help users start quickly with pre-formatted documents. The editor itself comes packed with features you'd expect from a modern document editor. You will learn how to implement different heading styles, bullet lists, checklists and links. We'll also cover more complex features like table insertion, font customization and even image uploads. On top of that, users will be able to adjust their margins and also export their documents in various formats including HTML, PDF, plain text and JSON. Our project will support both personal and organization accounts. Users will be able to switch between these in the navigation bar. We will also have a fully working invite system for organizations. You'll see how team members can receive and accept invites to join an organization. We'll be building all of these features step by step using a modern tech stack, including Next.js 15, React 19, Convex, LiveBlocks, and Clerc. This project will give you hands-on experience with real-time synchronization, rich text editing and user management systems. By the end of this course, you'll have built a powerful document editor that demonstrates your ability to create complex and interactive web applications. And now, without further ado, let's get started. Let's go ahead and let's configure our project. In order to do that, we first have to confirm our system requirements. As you can see, the minimum node version required to run Next.js is 18.18. You can go ahead and go inside of your terminal and type node-v to see your version. In case your version is too low or you've gotten an error that this command doesn't even exist, you have to go ahead and install the latest Node.js long-term support version. you have confirmed that, what you can do is also check out your npm and npx versions here. So just ensure you have both of these packages available inside of your environment. One important thing to note here is that we're going to be using Next.js 15. Why is that important? Well, it's important because Next.js 15 has already adopted React 19. But at the moment I'm recording this video, React 19 is only a release candidate, meaning it is not a stable React version. So why does that matter? Well, it matters if we plan on installing any packages from NPM. So this is a nice explanation of what I'm trying to explain to you here. As I've just mentioned, React 19 is a release candidate, and it is tested and supported in the latest Next.js 15 release, which is what we are going to be using today. But there is an issue. There is an issue because other package maintainers need to test and update their packages to include React 19 as a peer dependency. And this is already in progress for a lot of famous packages. So this is basically the change that a lot of package maintainers have to do. But before they can just add a new version, they also have to test that nothing has been broken. So what happens if a package we want to use did not include React 19 inside of peer dependencies? Well, if you are using npm, for example, npm install, right? If you're using that and a package does not have React 19, you will get this kind of error. And the reason this is important is because if you're using bun or pnpm, it will only show a silent warning. So that is a completely different behavior. And I believe that majority of my viewers are using npm. So for that reason, I will be using npm as well. So in case we encounter a package which throws this error, we are going to learn how to fix that together. So now let's go ahead and ignore all of this. This page is not really that important at the moment. I just wanted to give you a brief explanation of why I am using npm and not bun as I do in a lot of my recent other tutorials. So let's go ahead and let's run the automatic installation of a Next.js project. So instead of running the command npx create next app latest, I want you to do the following. npx create next app, and I want you to use 15.0.3. Why is that? Well, Next.js changes very fast. And if you want the best experience of following this tutorial and getting the exact same result as I do, the closer your versions are to mine, the better experience and end result you are going to have. So if you go ahead and run latest here, I don't know when you're watching this video. Perhaps that can already be Next.js 16 and a ton of stuff can change. So for that reason, let's go ahead and let's run npx create next app 15.0.3 together. So if you get prompted by this, you can go ahead and press Y. And now let's go ahead and let's name our app. So I'm going to go ahead and call this docs tutorial. I will be using TypeScript, Eslint. I will be using Tailwind CSS. And I will be using the source directory. So if yours is selected as no, you can use arrow keys to move it left and right to select yes. Also, make sure you select the app router. I will not be using Turbo Pack, so I will leave this at no. And I will not customize the import alias, which is the at sign by default. So I will just press enter here. And there we go. We are now installing our project. So I'm going to pause my video here until this is installed. And there we go. Once you see success and created your project name, you know it has been created. And I just want to show you that I did get some warnings here. I will not be resolving them at the moment. I just want to tell you that in case you are getting the warnings, I'm getting them as well. It's completely okay. We can continue building. So now that we know that we have our project installed, go ahead and open your IDE. In my case, I'm going to be using Visual Studio Code. So I'm going to select open here and I will select Doc's tutorial right here. If you get this prompt, you can press yes, I trust the authors right here. And now what I want to do is I want to go over our project structure here. So we ensure that we have the same things installed. So I have the source folder because when we installed our Next.js, we selected yes for the source directory. So inside of the source folder, we can find the app folder where we are going to build our Next.js application. Right now, if you go inside of your app folder, you will see a page.tsx. You will see a layout file, a globals.css. You will see the favicon right here, and you will have some gaste mono fonts. Besides that, you will have some config files like eslint, gitignore, nextconfig. And let's go ahead and, well, this is not exactly a config file. Well, I guess it is for your package manager, right? But go ahead and go inside of package.json. The reason I want you to go inside is to confirm that you have the dependencies that I do here. So as you can see, my next version is 15.0.3 and my React and my React DOM versions are 19.0 with the suffix of RC meaning release candidate right here. This exact number, I believe, doesn't really matter as long as you have installed next 15.0.3 I think that you will get the exact same version, but even if you didn't, as long as it's 19, that's it. That's what I wanted you to do. You can also pause the screen and just check out these dev dependencies as well, just in case you see that something is obviously wrong. So you can perhaps start again before you go ahead and build the entire project with wrong dependencies. But as I said, if you have run the 15.0.3, you will most likely have exactly the same things as I do. Great. So besides package.json, what's important is our tailwind.config.ts. So this is important because this was automatically created for us. But usually when you set up Tailwind yourself, right? Because in this, let me see, would you like to use Tailwind? We selected yes, right? So they've set up Tailwind for us, which means they have added the routes where Tailwind is used. So why am I teaching you this? Well, if you plan on straying away from the structure I will be building in this project, and if you plan to, for example, I don't know, create a new folder inside of source called primitives, like this, and then inside you create your own button and you start using Tailwind here Keep in mind that Tailwind will not work inside of this folder Why will it not work inside Well because of this TailwindConfig So if you plan on doing any custom folders inside of the source which are not pages or components or the app folder, remember that you will have to add it here. So if I wanted my primitives folder to work, I will have to do this exact thing here, right? So I just wanted to kind of briefly explain that. But again, if you are planning on following exactly what I'm doing, you will have no issues. So I'm now going to remove this primitives folder, which I have just created for demonstration. So my entire project will be inside of the app folder and then inside of the components folder, but all of them will be inside of source here. Great. So just double check that your tailwindow on config.ts is exactly the same as mine. Otherwise, you will run into some styling problems. And we can also check the tsconfig here. Well, a lot of things are written here, but I never modify this. And I've never really noticed any issues with this being different. Great. Let's go ahead and let's start the project now. So in order to start a project, you're going to run npx run dev. My apologies, npm run dev. And I made a mistake here because I forgot to do something. So once we create our docs tutorial, we have to enter that. So let's go inside of cd docs tutorial right here. There we go. So if I do npm run dev now, there we go. We can now access our app on localhost. So I'm going to go ahead and copy this. There we go. So Next.js 15.0.3, make sure you have the same version. And I will now go ahead and visit my localhost 3000 here. And what I'm expecting is just a Next.js welcome screen. In your case, you might see a white background. That is because if you take a look inside of our source app folder globals, you can see that they have the color scheme. So media prefers color scheme dark. So if I go ahead and comment this out, for example, there we go. You can see that now it is white. So you can leave it like this for now. And now what I want to do next is set up ShatZN UI, which we are going to use to build our own component library. Because right now we have no components here, right? We only have this page. We have the layout, which we are going to explain a bit later. So what I want to do now is I want to go to ShadCN UI. Let me show you the URL. So ui.shadcn.com. You can go ahead and visit this. So build your component library. And let's go inside of docs here. So what is ShadCN UI? It is not a component library. It's a collection of reusable components that we can copy and paste inside of our apps. What does that mean? Well, it means that we are not going to install ShatZN as a dependency, right? It's not available or distributed via NPM, but it is built on top of Radix, right? So you're going to see how this is used in a moment. Let's head inside of the installation. Let's go inside of Next.js here. So this is what I'm going to do. As you can see again here, it tells you to use NPX ShatZN at latest, but we are not going to be using the latest version. Let's fixate it to exact version that I have. So the version that is currently the latest is the following. Let me show you. Do not install this yet. So npx chat scene at latest current version is 2.1.6. So that's what we're going to do. Let me close this. And I'm now going to shut down my npm run dev here. and I'm going to go ahead and run npx chatcn at latest, and I already forgot my version here, so 2.1.6, so npx chatcn at latest, my apologies, at 2.1.6, and then init like this, so that's how we ensure that you have the exact same version that I have, if you get prompted with this, go ahead and press yes right here. Now let's wait a bit for this to install. There we go. It has already been installed. We're playing framework. So framework has been found. Next.js. One important thing to mention is make sure you are doing this inside of your project directory, right? So don't do this outside of your project. It needs to be inside of docs tutorial or whatever you have named. And as you can see, I have entered my directory here. Great. So you need to run that inside of here. We can select New York for our style. We can select neutral for our base color. And we can select yes for CSS variables for theming right here. And there we go. Now we have this question. It looks like you're using React 19, which is true because we are using NextRes 15. Some packages may fail to install due to peer dependency issues in NPM. So we can resolve this in two ways, either using force or by using legacy peer depths. I'm going to be using my arrow keys to go down and select legacy peer depths. You can use force and it's going to work just as well. But I have kind of explored the difference between the two. And I think that legacy peer depths might be a slightly better solution. But I have also found some arguments to use force as well. But as long as you do exactly as I do, you will be on the same page as I am. So let's select legacy peer depths here. And let's go ahead and install everything we need. Great. So let's quickly go inside of our project now and let's see what has changed. As you can see, I have six unsaved changes here. So now I have this components.json file, which is basically my config for Shatsy and UI. So my style is New York. I am using the app router. I'm using TypeScript and I have my Tailwind connected here. So it found my file. It found my globals. I have selected neutral as the base color. And it also added some aliases like my components, my utils, my UI, my lib, and my hooks. And it has also installed a icon library called Lucid. So if you go inside of your package JSON, you will see some new packages here. Class Variance Authority, CLSX, Lucid React. You will see that, well, my Next.js has not just been installed. It was moved from down here to up here now. So Next.js is the same, but we do have new Tailwind Merge and Tailwind CSS Animate. And I believe that if you have used NPX ChatCN 2.1.6 in it, all of these versions will be exactly the same as mine. Great. We also have some modifications inside of our Tailwind.config.ts. So in case you have previously added something here yourself, just make sure it didn't get overridden, even though you didn't have to change anything here from before, right? It seems like we have some type error here or lint error. I'm going to leave this. I'm not going to change it. I don't think this will matter when it comes to building the app. And my globals.css has also been modified, as you can see here. So I believe that right now the automatic dark and light mode has been removed. And we also have the new utils.ts file, which has this handy cnutil, which we're going to be using to safely merge Tailwind classes. You will see that in action very, very soon. So what else has been added? Well, inside of our, well, actually nothing yet. I thought something was added, but it was not. Okay. So great. We now have chat CN, but let's see what has actually changed. So inside of your project, let's do npx run dev again. My apologies, npm run dev. Usually I use bun, so that's why I get confused. My apologies if that happens. So let me refresh this and see. There we go. So what has changed? Well, even though my entire machine is in dark mode, you can see that I no longer have that automatic dark mode. So that's the only noticeable thing that has changed at the moment. So what do I want to do next? I want to add Shadzian components next. As you can see, if you go inside of components here, you can, for example, select a button component. And inside of here, you get instructions on how to add it. But you can also add all components at once. and I will be doing that instead because it's simply easier to do it right now at this moment with the exact version that we have rather than, you know, do it only when we need it because I might forget the proper version. I might accidentally use at latest or something like that. And I just want you to have a nice experience following this tutorial. So this is what we are going to do. I will shut down the app just in case I don't want any hot reloading mess happening here. And I will do npx chat cn. Let me remind myself what is the version. So 2.1.6. So npx chat cn at 2.1.6. And we're going to go ahead and write add. And instead of specifying which component, let's just add dash dash all. And what I expect now is that all components are going to be added. And as you can see, we have some conflicts here. So again, I'm going to use my arrow keys to select the option use dash dash legacy peer depths. If you have used force before, then just use force again. I think it's not really good to mix the two solutions. And as I've said previously, I think legacy peer depths is a slightly better solution here. Great. So now it's installing all the possible components. So I'm going to pause the video and unpause when it's finished. And there we go. We have all Shatsian components installed now And let go ahead and run our npm run dev again let refresh our browser to ensure that it synchronized with our new command which has just been run and now let's go inside of source app folder page which represents what we're seeing right here and what i like to do is remove the entire thing and then let's go ahead and build it again So we're going to do a default export of home like this. And inside of here, we can add a div like this. And let's go ahead and test out our tailwind a little bit. So first, I'm just going to add a paragraph here. Hello, world. So what's important inside of this page.tsx files when it comes to Next.js is that they have an export default. If you just did an export const like this and forgot your default export, you are going to get an error here. So whenever you're working with a page file or a layout file, which we are going to explore later, make sure to always have a default export and you don't need the named export at all. There we go. We now have hello world right here. I will zoom in a lot. So your text will definitely not be as big as mine. So now let's test out our tailwind here. And I will show you some useful extensions that I have for tailwind. So let's go ahead and, for example, write flex minimum height of screen, items center, and justify center. And let's see what happens. As you can see, I have centered my text in the middle of the screen. So how come that when I type something, I have all of these recommendations from Tailwind? Well, that is thanks to all of these extensions, which I have. So I'm going to show you all of them. So I have color highlight. I have error lens, S-lint, git lens. I'm using material icon theme. I'm using Monokai Pro theme. I'm using Prisma, which we are not going to need in this tutorial. I'm using simple react snippets, which you might have seen me do this. So that is where that comes from. And this is the package, which I'm talking about tailwind CSS intelligence. Please install this package. It will help you so much. So if you only want a couple of packages from here, make sure it's tailwind CSS and make sure it's color highlight. I think color highlight is so helpful and error lens helps me personally. but it might be a choice. So another thing that Tailwind CSS IntelliSense will help you with is colors. So if I chose red 500, you can see that it will tell me exactly which color that is, right? So I don't have to guess. Or if I accidentally write something that doesn't exist, it will tell me that it doesn't exist, not by an error, but I will see that there is no color next to it. So I know something is wrong. And if you personally aren't sure if something exists, so for example, justify left, if you hover over this, nothing will happen because this command, this Tailwind class does not exist. But if you hover on justify center, it will show you the exact CSS happening inside. So that's why I think the Tailwind CSS IntelliSense is a very, very useful extension to have here. Great. So we confirmed that our Tailwind is working. And now let's try and import one of the packages which we have just added from ShatCN. So I have 57 unsaved changes here, which are basically all of the components which have just been added. So let's go ahead and try out a very simple one. So your components are located inside of source components here and then inside of the UI folder. Confirm that you have the button here. There we go. This is my button component. And now let's go ahead and import it. So we can import button from components UI button. And inside of here, I can now edit button click me like this. And there we go. We have a nice button in the middle of our screen. So what's the cool thing with ChatCN? Well, if you remember, we did not install this as a dependency, right? We are literally having the button code inside of our project. So whatever we want to modify here, we can do so. For example, I can go ahead and add a variant here of destructive like this. But let's say I don't like how it looks. I can go directly inside of the button code. I can go to the destructive variant and I can change this to green like this. And there we go. Now it's green, right? I'm going to revert that change now. And you can also just as easily, you know, create your own primary BGBlue 500, for example. And you can go right here. And now you can see that we now have primary here. There we go. Now it's blue. So that's the power of Shatsen. I have just removed that primary because we are not going to need it. And there we go. We now have an error from TypeScript here. So that's the power of Shatsen because it gives you full control over your components. You can modify them however you want. You can modify the height, the padding, the radius, anything you want, you can modify here. Great. So we have learned how to set up Next.js. We have explained some issues we might face using React 19 release candidate and Next.15. But we have also learned how to resolve them. We also added a lot of components, a lot of new packages, and we have learned how to add them to the root page of Next.js. So what we're going to do in the next chapter is slowly explore how the structure works. And we are going to add our initial editor using TipTap. And we're going to start building it from scratch to something usable similar to Google Docs or Microsoft Word. Great, great job. Now let's learn how to create routes, nested routes, and dynamic routes. Head inside of your app folder and create a new folder called documents. Inside of documents, we're going to create a reserved file name page, in our case .tsx. What's important inside of this page reserved file names for them to work and for them to transform this documents folder into a route, it needs to have, first of all, a reserved file name page. Second of all, it needs to be inside of the app folder. And third of all, it needs to have a default export. So the name of the export default does not matter. So you can call this documents page, you can call this X, whatever you want, it needs to be exported via export default. So I'm going to return a documents page here. And now if I go to local host 3000 slash documents, we are going to be able to see that page slash documents. There we go. Documents page. So now let's go ahead and create a nested route. So if I go ahead inside of documents and I create one to three, for example, and then another page dot DSX inside, and again, follow the rule. It needs to have a default export. So I'm going to call this document page because it's a single document. So document page like this. I will simply go inside and I will render one, two, three document or let's do this document ID one, two, three. And let's call this document ID page like that. So now if I go to localhost 3000 slash documents slash one, two, three, I will see that document here. There we go. Document ID one, two, three, but there is an issue, right? So most document IDs or user IDs, right? We are just imagining now that we have an entity called documents and we are kind of creating the routing for them, right? We are never going to hard code the ID of an individual document, right? So if I want to go to, for example, three to one, I will get a not found page. So how do we resolve this? Well, we resolve this by not having a hard coded name of the folder. So let me completely remove this folder and everything inside. And instead, inside of the documents, I'm going to create a new folder and I will be using square brackets. And inside, I will create a document ID. It's important that whatever you name here is exactly as you want it to be used inside of page.tsx or inside of any other hooks, which will read the route params. So be mindful of capitalization, because if you write it with a lowercase, you can only find it by writing it exactly like this. So be careful. So document ID in camel case, right? It doesn't matter, of course, but this is a convention that I will be using. So document ID inside of square brackets. And then I'm going to go ahead and call page.dsx again here. And I will call this a document ID page. And now let's do document ID again. Right now, you can see that I no longer have any errors. So I can go to documents 3 to 1. I can go to a completely random number and there are no errors. But how do we fetch this value here? Well, we can do that by creating an interface document ID page here. ID page props, my apologies. And inside of here, we can add params. We can go ahead and call this a promise like this. And inside of here, we're going to get a document ID, which will be a string. And then we can go ahead and apply that here. And then we're going to have the params. So now I can go ahead and do const document ID to be await params dot document ID. And let's turn this into an asynchronous method like this. So let me just see. Or I did not use this correctly, so let's do it like this. awaited params await params and then const document id will be awaited params dot document id like this and we can display document ID inside And there we go Now, no matter which one I type, I can exactly see above. So you might be thinking, why is this a promise? It actually wasn't a promise up until Next.js 15, which is the version we're using for the first time on my channel today. So that's why I changed this to this kind of structure. Now, I think that you can still do this, right? So you can still attempt to do this and it will most certainly work. The issue is I'm not exactly sure how or when, but it will throw an error because you need to use await here. So let's go ahead and take a look. maybe they're seeing here. There we go. So because this is a server component, the errors I'm getting are in the terminal. So I want you to do the following. Just remove the await, right? So just very simply do this. And as you can see here, it says use params.document.id. Params should be awaited before using its properties. And you can learn more here. So let me go ahead and open that here. There we go. Dynamic APS are asynchronous. So in here, you can learn more about why this happens. So there we go. This is what we currently have, right? We just destructured the params and we attempt a direct access of params.id, right? So that's what we are doing here. While that obviously still works, as you can see, it's not how you're supposed to do it anymore, right? So they give you one possible way to fix it. This is if you are upgrading on a large project. So they've created this command, which will search through your entire code base and fix that for you. But the fix is this, what we have done initially, right? There we go. They have a nicer solution than I did. So I'm going to go ahead and destructure document ID from await params. There we go. So no errors anymore. still works as intended and also no more errors here. So let me go ahead and add a lot of empty space here. So we confirm no errors are showing anymore, no matter what I type now. You can see that, whoops, let me just go back to my local host. 3000 documents test. There we go. So everything is working. So yes, just as it's written here, From now on, you can access the params using await params. That is the proper way of doing it. And in the client, you can use use from React, right? Asynchronous access like that. You can go ahead and read more about this if you're interested about why this is happening. But this is only required inside of Next.js 15, like that. So this is the first new thing we are encountering. Great. So we've learned how to do that. So now what I want to show you is the layout folder and how does it work. So we already have the root layout here, which basically defines some, well, root rules for our app. For example, the font, right? Or the class name to the body. And you can see it also initializes the main template, HTML body, and then our children are rendered inside. We also have some reserved constants here like metadata, which is used to control what is shown inside of this Create Next App tabs, for example. So let's go ahead and let's go inside of our documents here and let's create a new file layout.tsx. Let's learn how to create it. Similarly to page, it needs a default export. So I'm going to call this documents layout. and I'm going to return back documents layout like this. So now if I go to any document route, either document slash, you know, one to three, or if I go to slash documents, the only thing I see is the documents layout. That's because layout works a little bit differently. So every layout file, besides having a reserved file name being inside of the app folder and having a default export also needs a children. So documents layout props will have children, which are a type of React, React node like this. So I'm going to go ahead and destructure the children here and I will simply render the children. And I also have to assign the type properly. And there we go. Now slash documents shows the documents page. And if I go to documents one to three, we have documents ID. So what did I exactly achieve here? Well, let's go ahead and do the following. Let's go ahead and add a paragraph here. Document navbar, for example. And let me just go ahead and give this a class name of flex, flex column, and gap y of four. And perhaps instead of this being a paragraph, this can be a navbar like this. And let's give this a class name of full width and background red 500. And there we go. So what did we achieve now? As you can see, this navbar, while it's only defined once, it will be shown inside of every document folder page. So if I'm on documents, any ID, it's shown here. If I'm just on slash documents, it's still shown here. If I, for example, create a settings folder inside of documents and then inside a page.dsx, right? And then a documents settings page, settings page for documents. And then I go to documents slash settings. There we go. The document navbar is still present. And the cool thing is that the layout does not re-render, right? So that's one thing you ought to know about the layout file. So that's what the layout file is useful for. when you want to create, well, reusable layouts, just as the name says. Great. And now I want to show you a different type of folder, which I think will be useful for you to know. So let's go inside of the app folder here. And for example, create an out folder. And then inside of here, create sign in and also create a sign up and then create a page.esx in one of them. So I'm going to call this sign up page, div sign up page. I will copy this entire page.dsx and I will paste it inside of another one and I will just rename this sign in, right? So now you already know how to access this, right? So localhost 3000 slash out slash sign in and slash sign up. So let's try it out. If I go to slash out slash sign in, I can see the sign in page. Or if I go to sign up, I can see the sign up page. Right. But I really don't like this out prefix here. Right. But at the same time, I would prefer having both of these routes encapsulated in a folder called out. Right. That's kind of a completely normal requirement to have. Right. I don't want these routes to just be sitting in my app folder since both of them are out related. I want to keep them in a folder called out, but I really dislike that they are part of the URL. So what we can do is we can rename this folder and put parentheses around the name. Once you do this, Visual Studio Code might ask you if you want to update imports for out because you just, you know, modified the next JS cache, right? Up until this point, it included the out in the routes, but from now on, it will not. So that's why it's asking you to update imports. You can press yes. You don't have to worry too much about this. And you might have one unsaved file pop up here. It's most likely going to be a file inside of this .next cache, right? What you can do is if that happens, you can just click on that unsaved file, click inside and just save it until it's saved. If you think you've messed it up, don't worry because this next folder can be removed as many times as you want. You can remove it right here and you can just restart your server, for example, and it will be rebuilt. So what has changed now? Instead of accessing our sign-in and sign-up routes through the out folder, we can now directly go to localhost3000 slash sign-up and slash sign-in. And they are still encapsulated inside of this route group, but this time the out folder is not a part of the URL. So that's a really cool feature to have. And here's another cool thing. I will copy the layout from my documents and I will paste it inside of the out folder. I will go inside and I will rename this from documents layout to out layout. And I'm going to call this out not bar. And there we go. So even though the out is omitted from the URL, you can still use it as you would use a regular route and it can have its own layout, right? So that's why I wanted to teach you that, right? If you really, really don't want documents as a part of your URL, but you want the structure of your Next.js folders to have the documents folder, you can just simply wrap it inside of parentheses. This means it won't be a part of the URL. its children will be, but you can still put layout inside. You can still put loading inside and all of those others reserved keywords. Great. So I think we have now a much greater knowledge of project structure. And what I want to do is, well, we are not going to be needing this out. So you can remove this. You can go directly inside of documents and you can remove the layout. We're You're not going to need it for now. You can also remove the settings. So I just want you to have app folder, documents, document ID, and normal documents page here. And now let's also wrap up the chapter by going back inside of our page.tsx, which is our route here. And let's learn how to do one thing. So I'm going to import link from next link. And I will add here, click link.\nhere to go to document ID. And inside of here, we can add an href slash documents slash 123, like this. So now I will go back to my localhost 3000 root page. And you can see that it's kind of separated here. So we can improve it. We can add a span here. Like this. We can go ahead and give it a class name, for example. We can give it a text blue 500 and text underline or just underline. Let's see. Yes, it's just underline. There we go. Now it looks more like a link. And we can also add some spaces here and here, I believe. Or we can do a Unicode at NBSP. so we can just add some white spaces. These are just, you know, details. You don't have to do that. But there we go. Now, when you click here, you will get redirected to document ID 123. So let me just collect this so you can see how it looks like. And I want you to have this on your, you know, root file. So you can simply click on here. If you're ever redirected back here, you can just click this and you will get redirected to a document ID page because what we're going to be building next is the TipTap editor. And we're going to be doing that inside of the document ID page right here. So that's why I want you to have a quick redirect from your root page to the individual document ID. And then later, we're going to connect that to the database. So it's actually going to have the whole screen of documents. And then you will pick and choose which document you want or to create a completely new one. Great. So you are now more familiar with the project structure. You can play around and create your own routes here. Later on, we will be meeting some new reserved files, which we are going to explain when the time comes. But most of them follow the exact same logic as we have explored right now. Great, great job. Now let's go ahead and let's implement the actual editor component, which will be rendered inside of every individual document ID page. So just as I've left off, ensure that on your root page, you have this quick redirect to an individual document one to three page, because this is where we're going to develop. And I don't want you to have to waste time typing the URL every time. Now go inside of source, inside of your app folder, documents, document ID. And inside of here, create a new file, editor.tsx. And inside of here, you can simply export const editor. So I'm going to be using named exports for components. And only for pages and layouts, I'm going to use the default export. So I'm going to go ahead and write editor here like that. And then let's go inside of page.tsx. And now let's just import the editor from .slash editor like that. And let's render the editor just below our document ID here. And there we go. We now have the editor right here. So what I want to do now is actually add TipTap inside of here. So let's go to the TipTap documentation page. Let's go ahead and you can use the link in the description to find this page or simply Google TipTap docs. Now let's go ahead and click on the editor here and let's click on install. I'm going to select the Next.js option. We have already created a project. And now let's install the dependencies. We're going to have to install TipTap React, TipTap PM, which stands for Prozmirror, and TipTap Starter Kit. But I want to be a bit more specific with our versions. So I'm going to prepare this here. You can ignore this. I've been testing things out, right? So you can ignore this completely for now. And this is what we're going to install. TipTap React, TipTap PM, and TipTap Starter Kit. But before I run this command, I have opened all of this on NPM. So you can see what is the latest version at the time I'm building this video. So for TipTap React, the latest version is 2.10.2. So I'm going to go ahead and add that specific version here like that. And lucky for me, all of them are the same. Well, it's not actually lucky. I think, of course, they purposely make that. But yeah, all of them should be the same version. So let me go ahead and try and expand this a bit more. There we go. Oops. So TipTap React 2.10.2, TipTap PM 2.10.2, and TipTap Starter Kit at 2.10.2. So I will just double check. There we go. All of them here are the same. And if you're wondering, TipTap does support React 19. So they didn't have to do any changes for React 19. And they actually have added React 19 in the peer dependencies. but if you go ahead and install this you will still get the error so what we have to do is we have to attach legacy peer depths right the same thing we had to do inside of our chat cn components so i'm going to go ahead and do this again npm install tip tap react 2.10.2 tip tap pros mirror 2.10.2 and tip tap starter kit 2.10.2 and then i'm going to add a dash dash legacy period apps flag here. Make sure you're doing this inside of the docs tutorial. And let's try this again and see if this time it's going to install without any errors. And there we go. So now if I go ahead and take a look here inside of my package JSON, I have tip tap PM 2.10.2, tip tap starter kit with the same version and tip tap react with the same version. There we go. So all three of them. I've messed up reading them, but yeah, all three of them are here with the same version. Great. So now let's go ahead and let's actually add the editor here. So what we have to do is we have to import use editor and editor content from TipTap React, and we have to import starter kit from TipTap starter kit. So I'm going to go ahead and do that here like this. And then I'm going to go ahead and define the editor here like this. And then I'm finally going to render the editor content like that. And I'm just going to pass in the editor prod like this. If I save this, seemingly everything is okay until I go inside of this page. And then we have an error. We have an error which says that class extends value undefined is not a constructor of knowledge, which is a horrible, horrible error, which tells you absolutely nothing. But thankfully, there is an explanation here. This might be caused by a React class component being rendered in a server component. So if you're new to Next.js, every component inside of the app folder is a server component by default, which means that you can directly fetch database with it. So server components have a bunch of great features, which I talk a lot in all of my other tutorials. So you can go ahead and visit them if you want, you know, an in-depth explanation of them. You can also, you know, visit the Next.js documentation. The problem with them is that they are static. You cannot really use hooks with them, like use editor. So in order to fix this issue, we have to turn this into a client component by adding use client here at the top. Once we do that, the error goes away. And as you can see here, we have our editor. And since we have added starter kit inside of here, we can try and select this and perhaps do Command B or Control B. And you can see that this becomes bold. Let's see if there's anything else we can do. So you can't really see it, but I'm actually writing bullet points here, right? There are a lot of things we can do here, and we're going to go ahead and do them step by step, right? But every time we refresh now, we're going to reset to this content. Later on, we're going to make sure that what we write is automatically saved to the database. But until we develop the entire editor, we will not be connecting to the database or anything else, right? But what we are going to do is we're going to implement the toolbar so that besides using, you know, shortcuts like command I to make this italic, we are going to also have a toolbar at the top to press that button right here. So that's what we're going to do. And we're also going to style this so it looks like an A4 paper, just like in Microsoft Word or in Google Docs. Now let's go ahead and style this a it. So I'm going to go back inside of page.tsx of the individual document ID. I'm going to remove the document ID text here. And I'm going to go ahead and give this a class name, minimum height of screen, and the background of FAFBFD, like that. Then I'm going to go inside of the editor right here, and I'm going to add some additional styles. So I'm going to go ahead and give this div a class name, size, full, overflow, x auto, background color is going to be f9, fb, fd, ex will be 4. When we print the page, I'm going to remove all padding so the document is as clear as possible for PDF reading. So padding will be 0 on print media query. On print, we're also going to use a plain white page. And on print, all overflow will be visible. So make sure you have added these three prints right here, because it's very important to make print as beautiful as possible for your docs clone. So now, and encapsulate this editor content inside of another div, and give this a class name of minimum width of max. Flex justify center width of 816 pixels. Py of 4 on print py0. So we are going to add some padding for us visually, but when we actually print this out, we want to be as plain as possible so it looks good in a PDF, right? And then let's use MX auto here to make sure this is centered in the middle of our screen. But on print, we're going to have full width. And on print, minimum width will be zero, like that. So ensure that you are having all of this. And already, you should see a slight difference. So now this editor right here is centered like that Great So now what I want to do is I want to go inside of the use editor here and I going to go ahead and add editor props here And I'm going to add attributes. And then I'm going to add class like this. Instead of this class, I'm going to add the following focus outline none. So once I've added this, you can see that I no longer get that ugly looking outline every time we focus. So now it kind of looks flashed out, right? Then let's go ahead and let's add on print border zero. Let's add background white. Let's add a border C7, C7, C7, like that. Let's add flex, flex column, a minimum height of 1054 pixels, like this. a width of 816 pixels, a padding top of 10, a PR of 14, a padding bottom of 10, and cursor to be text like this. So this is the entire class name inside of the inside attributes, right? So I haven't found a way to pass that through this component. You can try adding it to the class name. It will not work. So this is obviously targeting something else here. So there we go. You can see, I don't know how well you can see, right? I'm going to zoom out a bit, but you can see that we have a paper here, which is definitely different from what I have on the background. What I'm missing is a border. I did define a border color, but I forgot to actually add border class name here. So once I do that, it's definitely more visible than before. Perfect. So now what I want to do is I want to add some style here, right? I'm purposely doing this in style because later this will be dynamic, which means I cannot use Tailwind for that because of the how just-in-time compiler works for Tailwind. So we're going to add padding-left here, and we're going to set it to 56 pixels. And then I'm going to add padding-right, which will be 56 pixels as well. Right now, this doesn't make sense. Why am I writing this like that? But later, this will be dynamic, both this and this. So that's why I have already separated them. So right now, there we go. You can see that we already kind of look like Word, right? And if you go to, if I'm at 100% Zoom, you can see how the document looks like. So it definitely looks like the actual Google Docs or Microsoft Word size document. Great. So we have learned how to style our editor, both from the outside and from the inside. And if you try printing something, so I'm going to go ahead and do test print. And if I press command and print here, there you can see I can't exactly zoom in here. Maybe I can. Oh, I can. Okay, so let's see. You can see that we have this added from the print, right? So this is the time it is. We have the actual title of our tab. but you can see how there is nothing else included besides this. So we will have to fix this later, even though this is only in development. So I'm not sure if this little spark is actually an issue, but we're going to check out print every now and then to make sure it always looks good, right? Right now, it's not really that impressive, but later on when we add a whole nav bar in the toolbar, it's going to be impressive with how well it's going to look when it's printed. If you're wondering what does this flash actually do, it's a helpful util from Next.js in development mode only, which tells you if this is a static route or dynamic route. And you can also close that if you want to. Great. So now that we have added our initial editor here and, you know, you have played around with everything it can do, italic text, for example, bold, unbold, all of those things. I'm not sure what else does it have. Does it have headings? It does not yet. Great. So basically, why do we even have those features? Because of the starter kit. So starter kit is a collection of essential editor extensions. Which extensions are those? Well, basically, instead of tip-tap docs, if you go inside of the nodes here, you can see they have a bunch of stuff like block quote, bullet list, code block, document, heartbreak, heading, horizontal rule, image, list item, paragraph, table, all of those things, right? And they also have a starter kit. Let me try and find the actual starter kit using the starter kit extension. There we go. So this is what's included. Block quote, bullet list, code block, document, heartbreak, heading, horizontal rule, list item, ordered list, paragraph, and text. And also some marks like bold, code, italic, and strike, and also some extensions. So you can see we have included all of these things with one simple starter kit extension here. You can also, if you want for any reason, you can install any of this individually and you can add them next to this. So you can do this if you want to, right? And then you can extend it and configure it. But you can also configure everything you want from the starter kit, right? So if you want to play around with this, I'm going to do all of that later. So for now, I want to stop here. We've learned how to style the editor from the outside and from the inside. We've made it look kind of like an A4 paper. I'm quite satisfied with how it looks. And now what we are going to work on is, well, we're going to go ahead and kind of ensure that we have all the necessary extensions, right? Because you should be able to do a lot of things just via a keyboard, but we also have to read a toolbar so that you can click on it. So you don't have to do command B for bold, but that you can click on something, right? But also we have, we actually have bullet list implemented right now. So you use bullet list, like adding a dash here, right? And then adding space. You can see how it disappears because it actually turned into a bullet list. The problem is we don't have styles for it. So it doesn't look like anything. Right. So that's the kind of things we have to fix. And we're going to do that in the next chapter. I want you to go ahead and play around with TipTap if you want to. But we're going to stop here. And then in the next chapter, we're going to go ahead and expand all the extensions and see if we are ready to build the toolbar. Great. Great job. What I want to do now is enable styling for some of the extensions, which we actually have inside of Starter Kit, but we can't really see them. So I'm talking about bullet points, right? I've briefly explained this in the previous chapter. They are currently invisible. So let's go ahead and prepare this. I'm going to go ahead inside of source app folder, and I'm going to go inside of globals.css. I'm going to go all the way down where the file ends, and I'm going to open a TipTap class like this. And now what we're going to do is we're going to go inside of TipTap. And I want you to follow along here because we're going to have to copy and paste some styles. So let's go ahead and let's go inside of nodes. And let's go ahead and let's find, let's say we have a list item here. Let's try and find a normal bullet list. There we go. It was right here, the second one. So you're going to open the bullet list extension here, and you're going to go inside of styles.css like this. And then you can copy the list styles like this. In case this changes in the future or the documentation isn't as it was in my video, you can always pause the screen and just type it out ahead. So let's go inside of TipTap here and let's paste this. So I like to use these comments. So list styles, UL meaning unordered list and ordered list, and then the individual paragraph inside of the list here. So make sure you save this file. So now what I want to do is I briefly want to expand this, right? So this is what I'm going to do. So I'm going to stop this here. like this. So I'm going to change the ordered list to only have the padding and the margin here, right? And then inside of here, I'm going to add unordered list and list. So both of them here, and I'm going to open it. And inside of here, what I'm going to do is I'm going to add list style type will be disk, like this. And I'm going to remove list here and just leave the paragraph to be margin top 0.25 em and margin bottom 0.25 em like this and then what i'm going to do outside of this unordered list and list is i'm going to add ordered list and list list style type will be decimal like that and then i'm going to open a paragraph again and i can just copy this entire thing actually. So we have now created styles for an unordered list and here we have styles for an ordered list and all of that is inside of the tip-top class here, right? So what we did is we copied from here but then we briefly modified it. So we use this as the general styles for ordered list and unordered list. And then we style the unordered list to use the list style type of disk and ordered list to use the list style type of decimal. Like that. There we go. So make sure you have this. And now I believe that if you go ahead and try it out, so I'm gonna refresh the entire thing. So if you add a dash and add space, it will turn into a bullet list. So list one, list two, list three. There we go. And I think that if I try this, there we go. Numbered list works perfectly. There we go. So that works because we have specified that for ordered list, the list style type will be decimal. And for unordered list, the list style type will be disk. Great. So we now have both our bullet points and ordered list working. Great. So now what I want to do is the following. While we are on the topic of lists, let's go ahead and do the following. Let's see if we already have task list. So task list allows checklists like this To be honest I not sure if this is included in the starter kit So I going to search for starter kit I'm going to open it in a new tab. So we have it here. Let's see. I think that task list does not exist here. Great. So let me just go ahead and see. So we type this and a new line, and it will magically transform to a task list. So right now, if we try that, it doesn't work. Okay. So it means that we have to install it. So in here, we can see the version. So 2.10.2. So I'm going to go ahead and do that. So I'm simply going to add extension task list and extension task item. I believe both of them are exactly the same version, 2.10.2. So let's go back inside of our project here. and let's do npm install like this but i will also add the proper versions here so i want you to do the same so you have no errors so 2.10.2 and for this one as well add 2.10.2 let's see if we're going to get errors i forgot to add legacy depths there we go so i have to add dash dash legacy-peer-depths like this. And there we go. So we now have those two, but we're not done yet. We now have to add task list and task item here. So let's import task list and task item from extension, from which we just installed, right? So let's go inside of documents, document ID editor here. and now I'm going to add those two. So task item and task list and you add them inside of this array of extensions. So after starter kit, I'm going to add task item and task list. And inside of here, it looks like we can configure the task item to be nested, right? So I'm going to add that configure here, nested, and let's enable it like this. And what's currently happening, I believe, I'm going to refresh the entire thing. If I add this, there we go. It looks like it works, right? But it's obviously kind of weirdly styled. So if I press enter, you can see I am in the list, but it's weird that the check mark is above the text. So what we have to do is we have to add styles for that. And that's why I want to do this while we are inside of the list styles so we don't go back and forth, right? So let's go inside of task list here. And let's go and click on styles.scss here. And then you can ignore everything from above because this is what we've just written out and added some special styles for that. And now I want to add this task list specific styles. So I'm going to go ahead and copy this and let's see how low it goes. So all the way here, I believe, I think it's including all the way this, the last one. I'm going to paste that now. So we're going to see. So that includes the UL, the list, the label, the div, the input, the UL, and that's it actually. Okay, let's see. So I'm going to go ahead and go after this. So after our list type decimal, I'm going to paste everything here. And let me just see. Yes, that was correct. So we need UL data type task list with this styles. Then we target individual list here. Then we target the label. Then we target the div. then we close this list then we open the input type checkbox and ul data type task list like that there we go perfect so now i'm gonna go ahead and check that out and there we go so this already looks much better so now if you i type uh if i type the i'm trying to think what this is called the square bracket, my apologies. So if I type square bracket and add space, it will turn into an actual checkbox here. Perfect. What I am noticing is that it's kind of not aligned. Let's see. Can we think of a way we can kind of improve that here? just a second. Maybe we can play around with flex start. Let's see, what does flex end do? Okay, so flex end kind of puts it in the opposite way, yeah. Can I do center? There we go, so center looks nice, but how does this look if it's multi-line? is what I was interested about. Yeah, so this is how it will look. If you hold shift enter, then it kind of looks weird. So then it's better to be, you know, flex start because then it's going to be here at the top. So I guess technically, yeah, this is the more correct way. So I'm actually not going to change anything. Okay, good. So now we've learned how to add a new extension, task item here. And let's actually try this out. Does it work with nested ones? It does. Perfect. So we can do all of this now. Excellent. Great. So now that we have added that, how about we go ahead and we now add our headings, right? Because let's go ahead and let's see. Is heading included in starter kit here? It is. So headings are included inside of starter kit. So we don't have to manually add them, but we do have to add their styles. So go ahead and click on heading here or simply find it inside of this extensions. And now we actually have to find the styles for that. So again, go ahead and find your headings here, go inside of styles.scss, and you're going to copy the heading styles, all of this right here to the end like that. And then we're going to go back inside of our globals.css. And I'm actually going to add them at the top of my tip tab here. It's easier to follow that way. There we go. So I have just added all of those here. Heading styles. So all of these, H1, H1, H2, H3, H4, H5, H6, all of those right here. And we can now try them out again. So I'm going to refresh the entire thing. And if I now add a hashtag and start typing, there we go. That's a big text. If I add two hashtags and start typing, then that's a bit smaller. If I add three, that is even smaller. And let's see, do we support four? That is even smaller. Yes, we do. And do we support five? That is even smaller. Perhaps that is the limit. I think these two are the same. But great. You can see that the space between the main heading and other headings gradually becomes smaller and smaller. So we have added the proper heading styles and no need to add any new extensions because StarterKit includes the heading extension. Great. So what I want to add now is two more styles, because I think I want to wrap up all the styling so that we have a ease of mind when it comes to that. So let's see, instead of starter kit, do we actually have a table? We don't have table, so we're going to add table. So I'm going to go ahead and go inside of my extensions, and let's see, is it here or is it somewhere else. It's here. Table. Okay. Version is 2.10.2. Great. So we're going to develop this now. The only issue is I don't think... Okay. So we're going to be able to copy the content. So we're going to see whether that works, whether our styling works or not. Because as far as I know, there is no way to use like a shortcut. Like we used, you know, a dash to turn a bullet list. We can't transform into a table like that. We have to implement the actual command to do that, like this, but we're going to do that later in our toolbar. So for now, we're going to have to do this npm install. So let's do this again. I'm just going to go ahead and quickly confirm in my other project if the versions are as I think they are and they are. So for all of this, You're going to have to add add 2.10.2 if you want to use the exact same version as I am. So make sure that you add 2.10.2 at the end of each of these versions. Just so you have the same experience as I do. And let's go ahead and use what is the command legacy peer depths. Like this. And let's see if this will work. I hope I didn't misspell anything or accidentally deleted some characters. Looks like I did not. Perfect. Let me go ahead and quickly show you my package JSON. I forgot to do that after we've added some packages here. Okay, so all of these are Radix related. We don't need to take a look at that. But there we go. TipTap extension table, table cell, table header, table row, task item, and the task list. So all of these are my new TipTap packages, which I have added. And you can see that everything tip-tap related in my project is this version right here. Great. So now we have tables. So let's go ahead and add that. So I'm going to import table row, table header, table cell, and table, all of them in the editor here. I'm going to add them like this. And then I'm just going to, you know, add them here. So let's add table, table, cell, table, header, table, row, all of them like this. Great. So no unused files now. And let's go inside of styles here. And then let's copy this table specific styling. So I'm going to copy it all the way to the end here like this. There we go. So now I'm going to go inside of globals.css. I'm going to go inside of my tip-tap class name here, and I will paste it here. There we go. Table specific styling. Perfect. So we have all of that here. We have an issue here because this is using a variable gray three, which, you know, they have defined somewhere in their styles, but we don't have this CSS variable. We also don't have this either or this. We also don't have this purple here. So I think we will have to modify this. I'm just taking a look if we did this anywhere else, but I didn't notice. Looks like we are not. So we are safe from that mistake. So what I want to do now is of course save this And now let replace each of this with actual styles we can do So I going to go ahead and replace this border here with just the color black like this Gray one. Let's see. Can I use maybe C7, C7, C7? Like this. Gray two. Let's see. C7, C7, C8, maybe. And for the purple one, well, I think we can just use dash dash primary. We can use primary because we actually have primary defined here because of shat cn. But, you know, I'm looking at my original source code now, and I didn't even modify these things, and my tables still look okay. So, I think this will be fine. I hope so. Okay. So, make sure you have just got rid of this unused variables, right? So we can use primary again, because we have primary here, confirm that you have it. If you don't, you can just add your own color here. You can, of course, play around and change the colors a bit more than I did. Okay, I can do this. Great. So how can I do this? This is because of the extension color highlight, right? So for the selected cell after, I kind of want to add, I guess, a darker gray color. I'm just going to pick one. There we go. 959596. Let's use that like this. There we go. And now, since we can't really test this out, right, there's nothing I can write here that will render a table. What we have to do is go inside of the table here and go ahead and just copy this initial content here. And then you can go inside of an editor and replace the content. So let me see, did I copy? I copied the content key as well. There we go. So just add this and let's go here and let's refresh. And there we go. You can see that we can now render our tables. Perfect. So you can go ahead and kind of keep this if you want to, and then play around with these colors, like the border, the background color, the background here, right? And of course, if you want to, you can go ahead and expand the table even further. You know, you can play around with resizing it and all of those things, but we're going to stop the table here. So what I want to do next is I want to wrap our tip-tap styles with images, right? So I think that inside of starter kit, we have image or maybe we don't have image. Let's see. So what we have to do is we have to find a random image that we can copy and paste. Let's see. I'm going to go ahead and go inside of extensions and find the image and just try and click copy on any of these placeholder images, which they have here, and try and insert them. Okay, so I tried to paste them and nothing happens. I think that's enough for us to confirm that we cannot insert images. So let's go ahead and let's install this. I thought that maybe image comes with the starter kit, but it looks like it does not. The version is 2.10.2. That's great. So let's go ahead and let's install this. I believe we're also going to have to do 2.10.2-legacy peer depths like this. There we go. So now let's try this again. Whoops. I'm going to go ahead and go and try and copy an image. So right-click, copy image. I'm going to refresh first. and it looks like I still cannot do it. And I think that that is because I might be missing some other extensions here which enable me to actually do that. So let me go ahead and see functionality. Maybe here, drop cursor. So that's used for dragging the image around. and I think this actually might be a part of the starter kit. Let's see. Yes, it is drop cursor here. So we don't have to install that. So I'm not sure why I am not able to see an image. So I'm just going to go ahead and pause the video and try and debug this myself. So I think the reason I'd be is that I've never added any image styles, right? So I'm going to go back inside of the image extension here. and let's go inside of styles like this and let's actually copy the image like this so i'm going to go inside of my globals above the table specific styling i'm just going to add a comment image specific styling now i'm going to remove this with primary i'm going to modify this to use primary because we have the primary variable, right? So now I have this right here. So I'm going to refresh this and I'm going to go and again try and copy an image. Try and paste it here. Looks like it's still not working. Okay, so I have to go ahead and pause and debug this. well i forgot to do a very very stupid thing i forgot to add the actual image extension but i think that's the issue okay so let's go ahead and actually import image from tip extension image right here and let's just add it here after starter kit let me try this again so i'm going to copy this i'm going to refresh this let me just there we go okay i was already scared for a second that is not working again but looks like it is working perfect so what i want to do now is i want to show you that there actually is a way to enable extension of this right and let's see we can drag the image great so we already have that from the starter kit but it'll be cool to like resize the image and there actually is a package for that which is not official so it's not from tip tap but we can i've tested it on my initial project and it did work as intendant so let's go ahead and do this let's do npm install tip tap dash extension resize image at 1.2.1 and let's see if i'm going to need to add, okay, so I need to add dash dash legacy peer depths to avoid the error here like that. Great. So we have this and now we have to import that inside of our editor. So let's go inside of the editor here. Let's import this. So image resize from tip tap dash extension resize image. You can see how it's different from all the other packages because it's not an official package, right? But it does work. So I'm just going to add image resize here. I'm going to refresh this. We need to grab an image again. So I'm just going to go ahead and do that. And there we go. You can see that I can now position it. I can center it. I can expand it. This bubble menu is not exactly beautiful, but you know if I think I can probably style it if you go ahead and yeah I guess you can so if you target this class pros mirror selected node and kind of target this you can style it but I'm not going to do that but yeah you can definitely style this and make it look a little bit better great so that works and what I want to do next is I actually want to start building my toolbar, right? Because we have established a lot of extensions now, and I think we are ready to actually implement the toolbar so we can see more of those things in actions. We definitely haven't added all the extensions which we're going to use, and we also have to develop some extensions on our own, like font family and font size, right? So that's what we're going to do next. Great, great job. Now let's go ahead and let's build the toolbar. So let's go back inside of our document ID page and let's go ahead and let's encapsulate this. Actually, no need to encapsulate anything, right? Let's just add a toolbar component like this. Let's go inside of document ID and let's add toolbar.tsx like that. Let's export const toolbar right here and let's return a div. toolbar let's go back inside of the document id page and let's import the toolbar from dot slash toolbar and now you should see the text toolbar right here above great now let's go ahead and let's style the toolbar so i'm going to give this div a class name of background f1 f4 f9 px of 2.5 Fpy of 0.5, rounded will be 24 pixels, minimum height will be 40 pixels, flex, items center, gap between each item will be 0.5, and overflow x auto, so it scrolls on mobile devices. So this is the entire class name. And then what we're going to do, let's just quickly see, can we see it already? And I can already see like a different colored toolbar here. So now what we ought to do is create a toolbar button component. So let's do const toolbar button. And let's go ahead and let's create an interface toolbar button props. Let's give it an onClick to be optional like this and void is active to be an optional boolean and icon to be a type of Lucid icon from Lucid react like this. Now let's go ahead and let's use that here. So where you can extract onClick is active\nand icon here. Let's immediately remap the icon to use icon with capital I so we can use it as the actual component here. And then inside of here, go ahead and return a native button element and simply render the icon inside. So that's why it's important to remap the icon from props to a capital I so it can be used as a component. And then let's go ahead and give this icon a class name of size four. And let's go ahead and give this button the following attributes. So on click will be on click. And then we're going to have class name here. But we are going to use curly brackets and we're going to import CN from lib utils. And we already have that. Remember, when we added chat CN UI, we got this CN function so that we can merge Tailwind classes safely and without conflicts. So it's inside of your app. No, it's inside of your source lib utils, right? So we have now imported that here. And then this is how it works. So you open up that method. In the first parameter, you write your default classes, right? So text small, height of seven, minimum width of seven, flex, items, center, justify center, rounded, small, hover, background, neutral, 200 with an 80% opacity. And then you add a comma. And in the second argument, you do dynamic classes. So if is active, in that case, override the background to be background, neutral, 280 like this. Now, I often get questions like, why do we have to use the CN? Couldn't we just use template literals and, you know, just do a thernary operation inside? You can, of course, do that. But keep in mind, you can often run into kind of weird bugs with tailwind and priority of the classes. What this package does for you is solves that, right? So a lot of things that you think you don't have to take care of, this util actually takes care of. So I highly recommend always using this when you're doing dynamic classes. And it has a nice syntax on top of everything. Great. So we now have the toolbar button. So what I want to do now is the following. I want to go ahead and I want to go back inside of my toolbar here. and I will define a const sections here, which will be an array like this. And inside of here, let's go ahead and let's define the type here. So this will be the following. Label will be a string. Icon will be a lucid icon. On click will be a void. And let me just collapse this. right so for now you know we are defining this to be an object so that's obviously incorrect because we just you know set it as an array here so on click will be a void and is active will be an optional boolean so what sections is going to be is going to be a matrix of those like this so make sure you put double arrays at the end so how exactly will that look like well inside of this array, we're going to open up another array like this. And then inside of here, an object. So for example, we're going to have undo. I can will be undo to icon from Lucid React. There we go. And then we're going to have an onclick method here. And well, I already started typing editor, but we don't really have that yet. So for now, I'm just going to add a console log undo clicked. okay so now that you have set up the sections here we can go ahead and actually render the sections so inside of here instead of rendering the toolbar text we're going to render the first array inside of our sections array so we're going to do sections first in the array dot map get the individual item here and then we're going to render a toolbar button the key will simply be item dot label and then we're just going to spread the item because the item props right here label icon on click and is active are compatible uh so looks like this this is some reason reading oh no it's i think this is just my typescript server so restart TypeScript server. And I think now, yeah, now it's okay. So yeah, you can see that this item is compatible with the toolbar button props, right? So that's why we can just spread the props here. So now we have an error here and it might be familiar. It's because toolbar needs to be a client component, right? Because we are using interactive things inside. So we have to go ahead and mark this as use client. The same thing we had to do in the editor, right? Because otherwise we get an error here. And there we go. You can see that now I have the undo button here, right? And if I go ahead and click this, you can see that undo clicked is logged. So what is our next step? Our next step is to actually enable the undo. Because right now, if you want to, you know, and you start typing something, You change this to bold. You change this to italic. And if you do Command Z or Control Z, if you're on window, you can see that you can actually undo. And you can also redo things. So history naturally exists inside of TipTap, inside of our starter kit. right? So our starter kit in the editor here actually includes history. So what we have to do now, let's just confirm that. Yeah. So extensions history inside of the starter kit here. So what we have to do now is we somehow have to pass this editor constant all the way inside of the toolbar here, which is kind of tricky because the editor and toolbar are next to each other in this component hierarchy, right? So we can't really pass it as a prop here. But what we can do is implement a global store. And we can do that using a couple of methods. We can use React context, we can use Sustand, or we can use Yotai, or of course, a billion other packages. I personally prefer Sustand, Sustand, I'm not sure how to pronounce it, and Yotai. But for this project, I'm going to be using SwishDent. So let's go ahead and look at the version that I have. So that is 5.0.1. So I'm going to go ahead and install that. Also, we have this error here, tip tap error. So we can also fix this in a moment. First of all, let's do npm install Zustand 5.1.0. My apologies, 5.0.1. Let's see if we are going to get the error. Looks like we have the error. So dash dash legacy peer depths. Let me expand this so you can see it in one line. There we go. So npm install Zustand at 5.0.1 dash dash legacy peer depths like this. And there we go. So if you're watching this in the future, you know, and you're not getting these errors, that means that, you know, React 19 has been adopted and you no longer have to do this. Right. So I'm doing this because, as I've explained in the beginning of the tutorial, not all package maintainers have added React 19 in their peer dependencies. And even with that, React 19 is still a release candidate. So I guess there is some mismatch between versions. Great. So now we have Sush 10 installed. So what I'm going to do is first of all, show you my package JSON. So let me just show you that. There we go. Sush 10 5.0.1. And then instead of my source, I'm going to create a new folder called store. And inside of here, I'm going to create use editor store.ts like this. And inside of here, I'm going to import create from Zustand. I'm going to import the editor from add tip tap slash react. And to be more exact, I will import type editor. And then I will create an interface editor state here to have the editor, which will be either the editor type or null. And set editor, which will accept the editor prop and void. And this can also be null, like this. And then let's create a constant use editor store here. Create editor state, like that. Let's extract set here, and let's immediately return an object, right? So if you do this, you are returning a function. But if you wrap this inside of parentheses, you are immediately returning an object. So you can add editor to be null by default and set editor to simply do its thing like this. There we go. And you can also do an export const use editor store. There we go. So that is our use editor store. So what we have to do now is we have to import that inside of our app documents editor right here. So I'm going to import use editor store from store use editor store here. And then what I'm going to do is I'm going to enable it here. So const use editor store set editor like this. and then we're going to go ahead and after our extensions but before the content or after the content or before the extensions it really does not matter you can even do it here yeah so we're going to add on create like this and you can actually use this autofill on create props like this and we can destructure this to be editor so once the editor has been created what we're going to do is we going to call set editor and pass it inside like this So once the editor initially gets created we going to have access to it wherever else we use this So for example, if I go inside of my toolbar and I add that here, not inside of the toolbar button, inside of my toolbar, right? So if I go ahead and add that here, use editor store, from store, use editor store here. And instead I get the editor, I can console log editor. And also let's add toolbar editor. Let's go ahead and try that out. So I'm going to open this up. I'm going to refresh. And there we go. In the beginning, toolbar editor is null, as you can see. And then once we load, the editor is actually an editor here. So what we can do then is the following. we can actually go ahead and find our onClick for the undo. And in this arrow function, we can do editor? chain.focus.undo.run. Like this. So now, I'm going to refresh. And then, I am going to type something. I am going to bold something. and then I'm going to press undo. And there we go. You can see how history is working here. So how did I know that I have to run this exact thing? Well, you can always visit the actual documentation. So let's open up history here. Let's go ahead and find history. And inside of here, you can find the instructions. And I mean, you can already see that everything kind of runs the same way. but let's go ahead and actually find the commands here uh oh why is it so populated with so many things i guess because this also includes like versioning and stuff but all i want is now i can find it when i want to demonstrate it for you uh but i promise it is exactly like this I mean, all of them are exactly like this, right? Great. So that works. But we're not entirely done with this part, right? So this updates the editor on create. But I want to keep it in sync for any possible event that can happen, right? When we type, when we change a font, when we select something, when we leave the editor, when we focus on the editor, when we get an error. I want to update my global store editor with all of those things. So let's go ahead and do the logical one, which is a cleanup on destroy. So on destroy set editor now, right? So if we unmount the editor, clean my global state. Then let's do an update here. Set editor, and I can destructure this editor here and pass in the editor like this. And then we can just copy and paste the following ones. Here, after on update, let's have on selection update. Like this. Again, update the editor. After that, let's have on transaction. After on transaction, let's have on focus. After on focus, let's have on blur. And last one will be on content error. Like this. So now, no matter what happens, our toolbar will be up to date with the editor content like this. You're, of course, free to explore your own solution on doing this. If you find out that this is a bad practice, leave a comment and show your solution. I'd be very interested to see how you've handled it. But I found this to work just fine. Great. So now that we've resolved that, we can focus on the toolbar here and we can go ahead and add some more things like redo and all the other buttons, all the other functionalities. And then when we find that some functionalities are not implemented, we're going to have to either install that extension or build it ourselves. Great, great job. Now let's go ahead and let's add some more tools to our toolbar. So we're going to use the same principle for creating the undo button. So we're going to add a new object inside of this first array. We're going to call it redo. Icon will be redo to icon from Lucid React. So make sure you add that import. And then on the click here will be an arrow function calling an editor, of course, with a question mark in case it's null. chain, focus, redo, and then run like that. So now if you go ahead and write something, you can undo and you can redo. Perfect. Now let's go ahead and add a third object here, which will have a label of print, an icon of printer icon from Lucid React. And on click here, we'll simply call window print. So we don't even need editor here. As always, make sure you have your icons imported here. When you click print here, there we go. So we will be needing to do some changes here. For example, we should not show the toolbar in our print query, but we will resolve that later. Great. So we have that. Let's go ahead and let's add a label spell check. let's go ahead and give it an icon of spell check icon from lucid react on click here will be an arrow function and i didn't really find any cool way of doing this but what we can do is we can enter the actual dom of the editor and we can attempt to get attribute called spell check So this is the native browser spell check, right? Nothing, no third party APIs here. And then what we can do is editor view DOM set attribute spell check. And then if current is equal to false inside of strings like that, set it to true, otherwise to false, right? So for example, right now, you can see how this has this red underline. But if I click this, it goes away. And if I click again, it goes back, right? So using pure browser APIs, we enabled a nice little spell check, right? Or at least a very good beginning of a spell check. You could later, of course, expand this to an actual third-party API to give some suggestions or things like that. Great. So what I want to do next is I want to open a next section of here. So right now we have this first array here, right? So now let's add another array here. So another set of objects here. So for example, we're going to add a label of bold like that. And we can add an icon, bold icon from Lucid React. and we can add on click here to be editor question mark chain focus toggle bold and run like that the issue is at the moment we cannot see that bold button and the reason for that is because we have to render that section as well but before we do that let's add a separator from components ui separator so you should have this component because in the beginning of our tutorial we added all components from ChatCN, and that includes the separator right here. So you can find that inside of your source components UI separator here. Great. So for this separator here, we're going to give it an orientation of vertical, and we're going to give it a class name of height 6 and background neutral 300, like that. And let me just fix this orientation. And why is this vertical is not assignable? Oh, I misspelled this, vertical. Okay, like that. So now, there we go. You can see how we have a separator here. So what I'm going to add now is the following. Inside of here, I'm just going to add some to do's. So to do font family. Then I'm going to add to do heading. then I'm going to add to do font size like this and between all of these we're going to have all of these are going to have their own separators like that so now we're just going to have four very very small separators but you can if you want to you can actually know add a span for example to do font size but it's just going to take a lot of space so you can just leave it like this in the code. So what we're going to do now is we're going to go ahead and go over sections. And this time, instead of using the first array, we're going to be using the second array block, dot map, item, like this, toolbar button, and simply a key item label, and item, like this. And there we go. We now have our bold button here. So if I go ahead and select a piece of text and click here, there we go. It becomes bold and we can toggle it on and off. One thing that's not happening at the moment is when I am inside of bold text, I would expect this to display an active state. And when I move back here, I would expect it to show this kind of state. So what we have to do is we have to go inside of our bold here and we have to add an is active element. So we're going to do that via editor. Is active bold. So now if I click here, there we go. You can see how it becomes active. Whereas when I move here, it's not active and same works as you can see when I move my cursor. Excellent. So that works. So let's go ahead and copy and paste this. Instead of bold, we are now going to have italic. So let's get our italic icon from Lucid React. It's going to check for italic and toggle italic. Let's quickly confirm whether it works. There we go Perfect Now let go ahead and copy and paste this After italic we going to have underline here Is active here will be underline And we going to use toggle underline Okay, so it looks like underline, total underline is not working. So I'm going to go ahead and explore why that is. So first, let's add underline icon here from Lucid React. So let's go ahead and make sure we have bold icon, italic icon, printer icon, all of these, right? So I think that the reason toggle underline is not working is because maybe it's not included in the starter kit. Let's see. It is not. So what we have to do is we have to go inside of marks, I believe, and go inside of underline here. And we have to install the extension, right? So the version here, I can't see the version. Let's see, will it appear? I think that the latest version is again 2.10.2. So let's do npm install extension underline like this. Let me just expand this. So 2.10.2 dash dash legacy here. So let's go ahead and install that again if you're watching this into the future go ahead and try without this command first right if it works no need to add legacy peer depths great so we now have the underlines and now we just have to make it work so let's import this let's go inside of our editor component here and let's import the underline and let's just add it in the list of our extensions after the starter kit like that. So now in my toolbar, no more do we have an error for the underline. So if I go here and select this, click, looks like I have an error. I think I just have to refresh this. So test, test, test. There we go. This is now underline. I can remove the spell check and it works. The active state also works. Great. So we have resolved that. Perfect. So now let's go ahead Then let's open another round of arrays here. So this one will have a label of message. This will be comments like this. Then we're going to have an icon of message square plus icon from Lucid React like that. So on click here will simply be console log to do comment. So we're going to do that later. Same as is active, which for now will be just false. And then we can add to do enable this functionality like that. So we have the comment here. So we now also have to render that here. So after this one, we're going to add some more to-dos here. So this will be a to-do text color. Then we're going to have to-do highlight color. Then we're going to have a separator like this. And then we're going to have to-do link. After link, we're going to have image. Then we're going to have a line. Then we're going to have line height. then we're going to have a list like this. And then we can go ahead and render our sections again like that. And this will be sections two. So this is the third array we are having. But remember, arrays start with zero, right? So that's why the last one, the third one, appears to be the second one. So these ones will be a little more complex. So that's why we cannot really put them inside of Array, right? Because we need the whole component for them, right? So the simple ones, I'm keeping them inside of Array. But the more complex ones, we're going to have to create in separate components and then add them here. Great. So sections two. And now let's focus on these sections. So there we go. We now have this comment, which currently does nothing. It's just logging something here. After the comment, let's go ahead and let's copy and paste this. This will be list to do. The icon will be list to do icon from Lucid React. And on click here will be editor, chain, focus, total task list, run. Like that. And for the is active, we can do editor, is active, task list. like that. So now if I go ahead and click this, there we go. This is a task list like this. Perfect. So I can go ahead and you can see that now it's not selected, but if I go up, you can see it appears selected because this is a task list. Perfect. And last one we're going to add, which are going to be these simple ones, will be the remove formatting label. So this will be remove formatting icon from Lucid React. And we're simply going to do unset all marks like this, and no need for is active. So just make sure you have remove formatting, spell check, underline, undo to message square plus, all of these new ones, which we have added. So now if you select everything and click this. For example, add some things here like this. And if you select all of these, it removes all formatting. Perfect. So that works flawlessly. So what we have to do next is we have to go ahead and develop all of these custom ones, like the font family, the heading, the font size, the text color, highlight color, link, image, align, line height, and list. Some of this will just need an additional extension, but some other like font family, and I believe font size as well, we're going to have to implement our own custom extension for that. So that's going to be quite interesting. What I want to do is first just kick off all the easy ones and then we can spend some time developing these ones. Great, great job. Let's continue by adding the first more complex tool than the ones we have in the list here. So that's going to be the font family. We're going to start doing that by first installing the font family extension. So if you go inside of extensions, functionality, you can find the font family. And what we have to do is we have to install that right here. So we need extension text style and extension font family. So I'm going to go ahead and copy both of those, paste them here. I'm going to pin their versions to 2.10.2. So same as this one, add 2.10.2. Go ahead and run this command, but I already know I'm going to get errors because React 19 at the moment I'm developing this project is a release candidate. So I need legacy peer tips like this. Again, you might not need this flag right here. Great. So we have those installed and now we have to go inside of the editor here and we just have to install. We have to add the font family import. So let me go inside of the editor here and let me add the font family. So I'm just going to add it right after starter kit here. There we go. Now we have font family. So we're going to go ahead and develop the font family component. So let's go ahead and do that inside of this toolbar component. No need to create a whole new component for that. If you prefer that, you can of course do it. I'm going to write it here. So font family button like this. And I'm going to get the editor from use editor plugin. My apologies, use editor store. We already have that imported, right? And then what we're going to do here is we're going to define an array of fonts we want to use, right so label ariel and value of ariel like this and then you can just go ahead and add any other fonts you really want for example pimes new roman uh career new so these are you know basic browser supported fonts like this if you want some special or custom fonts you can always spend a bit more time on this font family and explore what else they offer. You can also add some more interesting fonts if you want to. And you can, of course, follow the instructions of how to add some custom fonts if that's what you want. Great. So we now have our fonts defined here. And what we're going to have to do next is we're going to have to import everything we need from the dropdown menu. So what I'm going to do right here is I'm going to add drop down menu, drop down menu content, menu item, and menu trigger, all coming from components UI drop down menu, which we have installed in source components UI drop down menu. Great. So now let's go ahead and let's continue developing this. So this entire component will be one big drop down menu like this. Then inside, we're going to have a drop down menu trigger with an as child property so that it becomes whatever we put inside, which is going to be another button. If you don't put this, you're going to get an error because this is a button and then this is another button inside of that button. So that's why we use as child here. So we are using a native HTML button here. And let's go ahead and add a class name. And let's add CN. So we already have CN imported, I believe. So now we're going to merge some Tailwind classes. So let's do height of seven, a width of 120 pixels, a shrink is zero, flex, items center, justify between, rounded small, hover, background neutral, dash 200 slash 80 PX of 1 overflow hidden and text small like that And then go ahead and add a comma And my apologies we actually won be needing anything dynamic here I guess I planned ahead and thought that we will, but we won't. So if you want to, you can just use normal annotations here, like this. Great. So those are the class names. And now inside of the button, add a span. And inside of here, add a class name, truncate, like this. Now that we have added the, we have added, oh, I think I might have forgotten to add text style. Let me just confirm. Do I need to add text style? Yes, I forgot to add the text style extension. So we need that as well. So besides font family, we also installed text style. So I will just add that to the editor quickly after font family. There we go. So now let's go back inside of our font family button and let's add editor get attributes text style like this dot font family like this or we default to Arial like this. Then add a Chevron down icon from Lucid React and let's give it a class name. ml of 2, size of 4, and shrink of 0, like that. Outside of our button, and actually outside of the trigger at all, add a drop-down menu content. And inside of here, let's give it a class name, padding 1, flex, flex column, and gap Y of 1. Inside of here, we're going to iterate over this array of fonts, which we have created. So let's go ahead and do fonts.map. And we can immediately destructure from each font a label and the value like this. And then we're going to render a button. This button will have a key of value. So we immediately resolve that. I think I might have made some mistake here. Why is this not working? Oh, I think I have to wrap this in another parenthesis here. And then I have to add another parenthesis down here. There we go. So I was missing additional parenthesis here and one here. Okay. And now let's give this a class name. This time we will need CN here. So the first one will be flex item center gap X of 2, PX of 2, PY 1, rounded small and hover BG neutral 200 divided. My apologies with an 80% opacity. And then editor get attributes text style dot font family is equal to the current value. In that case, we're going to mark this as selected font. So neutral 200 slash 80. And then style will be font family value. So this is too dynamic for Tailwind. So the just-in-time compiler won't make this work, right? You can't do things like, I forgot how you do font, right? You can't do font and then value, right? That is too dynamic for just-in-time compiler. So it's safer to do it inside of style, right? And then inside of here, open a span and render the label like this. And let's give this a class name of text small. There we go. I believe that is it for our font family button. So now let's actually use it. Let's go down here and let's render font family button like this. Let's go back inside of our Tailwind. My apologies, our editor. So test, I'll select this and let's see, can I change it to Times New Roman? Looks like it's not working. So let's see, are we getting any errors or is this something else I'll have to debug? So let's start debugging this. oh i think i did not add on click on the button yes that is probably the issue so in each of these buttons we also have to add on click here and what we want to happen is we want to call the editor and chain focus set font family value run like this let's go ahead and give it another shot I'm just going to refresh this. Test. Let's try Times New Roman. And it is working. And you can see how it is selected, right? So if I go ahead and write test here, you can see that it's not selected. But here it's selected, right? So if I try this and select Georgia, perfect. Verdana, there we go. So we have developed. And you can see how nicely it truncates if it's too long, right? We have limited it to a certain width. Perfect. So now our font family button is working. So now let's go ahead and let's develop another custom component, which is going to be the heading, I believe. Yes, the heading. For the font size, I think we're going to have to implement our own extension. So we're going to leave that. We can do this. We can do this, this, this, this. And this will also require a custom extension, right? So we're going to leave the line height and the font size for last. But everything else, we should be able to do pretty easily using the same principles as we just did for the font family button. So let's go ahead and let's develop the heading level button like this. It's going to be very similar. So we can already copy this part, right? We're going to need access to our editor. and then let's define the headings. So similarly to defining our fonts, this time we are defining our headings here. So each of these are going to be an object with the label, which will be heading, let's start with normal text actually. So normal text, the value will be zero and font size will be 16 pixels because I wanna show that heading in the dropdown. So the same way we are already showing the fonts here so the user know how the font looks like, I wanna do the same thing for the heading level. I want the user to see how the heading level approximately looks like, right? So that's what we're gonna do next. So now let's go ahead and let's add, you know, heading one here. So that's value one and font size of 32 pixels. And then let's do heading two, which is value two and 24 pixels. After that, heading three, which is a value three and 20 pixels. and then we're going to have heading 4, value 4 and font size 18 pixels. And I think we can stop here. So heading 5 with a value 5 and font size of 16 pixels because this is already the same as normal text. Like that. There we go. So what we can do here is we can also, I believe, I'm not sure if I have to define. Yeah, I think this is just fine. Okay, so now what I want to do is I want to create a little util here, const get current heading. And inside of here, I'm going to open a for loop. So if level is one, before we reach the fifth heading like this, what we're going to do is the following. If editor is active, heading like that, and the current level of the heading. go ahead and return heading and that level like this. Otherwise, return normal text like that. So now, whoops, still inside of our heading level button here. Let's go ahead and let's return a drop down menu, a drop down menu trigger here and give it an as child property and now inside of here i think we can copy this button so i'm just going to copy this entire button from the drop down menu item here like that and we're just going to slightly style it so let's see class name uh height seven can stay the same. The width can be changed. So we can do this with the width. Let's do minimum width of seven. We don't care about anything else. And I think everything else is good. So I'm going to leave it as is like that. And then instead of justify between, I actually want to use justify center here like that everything else seems okay right so then inside of here we're simply going to call get current heading like that so this utile which we have created here which will recognize at what is the current level of the text we are in like this chevron down icon can stay the same that is good and the shrink rule i think that can stay here and then inside of here drop down menu content let's give the class name padding one flex flex column and gap y1 let me fix the typo here like this and inside of here we go over our headings So headings.map, open another parenthesis, and you can destructure the label, the value, and the font size. And then inside of here, we can return our button here, give it a key, my apologies, like this, give it a key of value, and we can give it a style font size, like that. So similarly to how we did style font family here. And I think that the class name can be copied from here. So let's copy it, go back here and paste it. But we are slightly going to modify the rule here. So get attributes. We're not going to do get attributes. We're going to slightly modify what is needed for this to become BG neutral. So open parenthesis. if value is equal to zero, and if there is no, or I mean, if the editor is not currently having an isActive heading, like that, or...\nIf editor is active, heading matches the current level to be value like this. In that case, we can go ahead and render this. So a bit more complex like that. So if value is zero and the editor is not actively on the heading, or if the editor is active and it matches the current level, then showcase that as currently selected value like that i think everything else is good and then inside of here we are simply going to showcase the label and i think that's it so now let's use our heading level button and let's add it to the toolbar the same way we have added the font family button there we go heading level button so now i'm going to go ahead and try and do this So if I change this to heading one, I again forgot to add the on click here. So let me add on click to this button. So if value is zero, what we're going to do is editor, chain, focus, set paragraph. So we are going to turn that into a paragraph, right? else editor dot chain focus toggle heading and we're going to pass in the level to be the current value and run now let's see what seems to be the issue here let's see my value here is a number but the level property here does not seem to be expected and i think i might know why that is. I'm not entirely sure. Let's see. Inside of my editor here, is there something I have to enable first? I think that I might have to somehow enable the headings because by default, the levels are not available on the heading. You have to somehow enable it. So let me just go ahead and quickly search for that. My apologies, that's not what's happening here. What's happening is that I am passing a value, which is a type of number, but inside of toggle heading, level is a type of level. So what I have to do here is, well, there are many ways we can do this, but what I think we can do is mark this as level. and I just have to find where exactly do I import the level from. So I think that I can import it from the extension heading package, but I'm not sure if we have that. So let me go ahead and try import type level from at tip tap slash extension and then heading. Do I have the extension heading? Because I already forgot. I'm not sure if this comes from the starter package. Let me go ahead and go inside of my package.json. I don't seem to have that. And I also don't seem to be getting an error here. So perhaps the starter kit has installed that. Nevertheless, I don't feel comfortable using an import that I don't have inside of my package.json. So what I'm going to do is I'm going to install it. 2.0 10.2 dash dash legacy here that's so i want to have it uh let's see do i have it now i do there we go it's right here so now when i have this package perfect i'm going to go ahead and use this type level and simply go ahead and define the value as level here let's save this and let's try this again. So this will now be a heading. I will go ahead and select this and click heading. And there we go. It turns into a heading. Works with heading two. Works with heading three. Works with heading four. And looks like heading five is not exactly. Yeah. Maybe we don't need Heading 5, you can, of course, decide that inside of, you know, globals.css. If you remember inside of TipTap, when we defined our heading specific styles right here, we can manually change how everything looks. So you can see that we have individual ones for heading 1, heading 2, heading 3. And then inside of here, we seem to all categorize this one as the same. You can modify this if you want to, right? Great. So that is working and you can see how it changes depending on where my cursor currently is. Perfect. So we have developed another custom component. So let's see what's next. So font size, we're going to skip that because it requires a custom extension. But what we can do is the text color. And we're going to do that in the next chapter. This one is already 20 minutes long. So I'm going to end this chapter here. And the next one, we're going to be doing the text color and the highlight color. Great, great job. Now let's implement the text color and the highlight color components. So in order to do that, we're going to have to install a package, react color. And let me go ahead and see what's the current version of that package. so at the moment the version is 2.19.3 so that's the version i'm going to be using so let's go ahead and do npm install react color at 2.19.3 let's see if we're going to get an error we are so we just have to add legacy here that's like this there we go we have that and we're also going to need to do something else. So we're going to have to add, I believe, two extensions, because if I go inside of functionalities starter kit, yeah, we don't have color and we don't have highlight. So we need those two. I think that's in marks. So inside of marks, you can find a highlight. So while I'm here, let's add that. I'm going to do NPM install TPEP extension highlight, the same newest version. So, NPM install tip-tap extension highlight at 2.10.2, legacy peer depths like this. There we go. And then I also need color, maybe. I'm not sure where they would put that. I would expect it to be here, but it's not. It is inside of functionality. So, color right here. It's the same thing. We already have text style, so we just need this one. So again, let's do this. NPM install at 2.10.2 legacy here depths like this. There we go. We already have text style as we concluded. So now we have the import color from TipTap extension color. It's quite odd that they use the main export for this, but you use default exports for all of this. I'm wondering if this is a mistake in the documentation. We're going to find out now. Let's go inside of the editor component here and let's import color. Yeah, they really did, but can I do this maybe? It's really odd that all of them are default exports, but they decided for this one to be like that. Okay. And let's import highlight from tip tap extension highlight. I butchered this. So let me go ahead and just fix it. So extension highlight. And I'm just unsure is highlight also imported like that no highlight is okay so only color is imported like this very interesting okay so after starter kit i will add color and highlight here and one thing that you have to add to highlight is multi-color true otherwise it will only be yellow. So configure multicolor and set it to true. There we go. So we now have both of them ready. And we're going to start with the text color because this one, well, they are both equally simple, but let's go ahead and do text color. So I'm going to go ahead and just go and build this at the top of my app here. So const text color button here. Let's go ahead and just copy this, no need to type it out if it's going to be the same, right? Let's get the current value, which will be editor, get attributes, text style dot color. If we cannot detect the color, we're going to assume we are using the color black and use the six character hex code because we are going to use the react color package and I'm unsure if they support this, right? So yeah, it's the same thing but use this one okay and then let's do on change here to accept the color which will be a color result you might be wondering where do we get the color result from well from the package we recently uh installed let me just move this here like that and then i I can import a type color result from react color. Oh, looks like there is no types for this. So we have to install types. Let me just go ahead and resolve that. So I just want to check what is the current version of that. So types, just a second. Let's see. Okay, so 3.0.12. Okay, so npm install save dev 3.0.12. I believe that, again, we have an error. So legacy here that's like this. There we go. And now there should be no error here. Perfect. Exactly what we need And besides this type we can already import the circle picker like this Great We can now go back inside of the text color button here So on change here what we're going to do is editor chain focus set color color.hex.run like that. and then inside of here we can go ahead and return a drop down menu a drop down menu trigger let's give it an as child prop here let's add a native button here and let's copy the class name from one of the other ones for example the heading component i'm just going to copy the class name here. All of them are pretty much the same. So I will just copy it here. And let's see, do we have to modify anything here? I think all of this is perfect. Nothing we have to change here. Great. Inside of this button here, I'm going to add a span. And I'm just going to type in the letter A. And I will give this a text extra small, like this. And then below that, I'm going to add a div. well, more specifically, a self-closing div with a class name of height 0.5, full width, and the style, which will be background color, which will be value like this. And then I'm going to add a dropdown, well, menu content. I'm going to give it a class name of padding of 2.5. So let me just close this. inside, I'm going to render the circle picker component. And I'm going to give it a color of value. I'm going to give it an on change of on change like this. So let's see how this looks like right now. All we have to do is we have to render the text color button. So let me go all the way down here. Where do I render it? There we go to do text color. So text color button. Okay. Kind of, not exactly. So I think what's missing here is flex column. So we did have to modify something for this dropdown menu trigger here. And there we go. So now it looks nice. So if I right test here and select this. Can I choose a color? And I can. You can see how nicely we can pick our colors here. Great. If you want to, for any reason, modify the circle picker CSS, we can kind of do that, I suppose. Well, there are many ways you can do to this color picker. So for example, you might notice that some basic colors are missing like white and black colors, right? So good news is that the circle picker, the React color itself gives you a bunch of different pickers. So we're using this one, like the circle picker, but you can choose any of these pickers if you want to give your users, you know, for example, you can choose the sketch picker. You can use this Photoshop picker or any of those, and they accept the exact same props. So you can go ahead and play around, for example, Sketch Picker, and then just use the Sketch Picker instead. All right, so if you want to, you know, you can use this as the color, like this. If you really want to use the Circle Picker, it also accepts colors, which can be an array, and then just, you know, a list of colors you are planning to accept. So you can do multiple things. And now that I think of it, I actually like the sketch picker more. So I'm going to be using the sketch picker here. Like this. I think it looks nice. There we go. And we can have a very, very good... We have a lot more choice when it comes to our colors. Great. Now, the only thing I dislike about this is like the double border. So, let's see, can I resolve that somehow? How about no padding? How about no border? Let's see. Kind of. Looks a little bit better. How about padding zero? And maybe yes to the border? Oh, yeah. This looks nice. I think we can handle this. Definitely better than the simplistic picker. But yeah, it's your choice. It offers a bunch of nice pickers for you to use. Great. So we developed that. Now what we have to do is the same, but for the highlight color. So what I'm going to do is I'm going to copy this entire component, the text color button, and I will just paste it above itself. And I'm going to rename this one to highlight color button. So now what we're going to do is the following. We don't need the current value. We just need onChange. Instead of setColor, we're going to be calling setHighlight here. And instead of here, we're going to do color to be color.hex like this. So that's going to be the only change in this methods here. This will be a bit more simplistic. So inside of the button, we're just going to render the highlighter icon from Lucid React. And let's give it a class name of size 4. So, make sure you have added this import here, like that. And for this, oh yeah, I guess, okay, we can do it like this. Let's see. So, if I go ahead, oh, I have to render this. My apologies. So, highlight the color button. Let me just try and render this first. Like this. Great. So if I select this, there we go. I can add so many nice highlights here. Oh, the only problem is, yeah, we can't really see the value of this. So yeah, we definitely need to find a way to get the current value of the highlights. So let me explore a bit. How can we do that? So how can I get the current value of the highlights? That's what I'm currently interested in. this is the toggle this is the set highlight but what we need is kind of a get attributes here let's see i'm going to pause the video and explore a bit how can i get the current highlight value so i'm going to try and do editor get attributes highlight i'm not sure if this will work and let's fall back to i guess white i'm not sure and let's try and set that as the value here let's see apparently this is not exactly working because this is a color is a type of any but let's see is it any better no not really okay i'm gonna try something else then all right so i did some uh funny mistakes here in order to get the current value of the highlight color you first have to get the editor get attributes highlight dot color like this and then you have to pass that as the color prop i was passing the value prop so that's why i was getting the error. So now we can see that when I select this, I fully control the current highlight color. And for highlight, I can actually assume that the default one is going to be all white, I suppose, or maybe it just doesn't exist. I'm not sure. But yeah, I think this is fine. Because yeah, it assumes that it's white, if nothing else is selected. Great. So we have now added the color and the highlight. Perfect. Great, great job. Now let's go ahead and let's implement the next complex tool here. So that's going to be the link and the image. In order to add link, we first have to add the link extension. So you can find that inside of extensions, marks link right here. So let's go ahead and let's install that. So I will do this. I will add a 2.10.2 and legacy peer depths like this. There we go. And besides that, we also have to add it to our editor. So let's go inside of the editor here. I'm going to add the link like this. And let's go ahead and add it after a starter kit. And let's go ahead and configure it. You can, of course, configure your own rules. So for example, open on click, I'm going to set that to false, right? I don't want the URL to open. I'm pretending that this is a Word document, right? I don't think that Word documents open links when you click on them. Auto link will be true. And default protocol here will be HTTPS. like that besides that we also need some specific styles here so we can copy these link styles for the href like this and let's go inside of globals right here and let's go ahead and just go to the top of our tip tap class and let's just add link styles and we don't have the purple variable here. So what I want to do here is for the color, I will simply use the tailwind at apply text blue 600 like this. And for the hover, I will simply use at apply underline. Make sure you add semicolons here. And I think that already, if you go ahead and try and write something. OK, it looks like it did not auto. Maybe I have the refresh. Let's try again. There we go. You can see how it auto links here. Great. So we can now use text to enable a link But what I want to do is I want to create a component where user will be able to manually insert a link So let's go ahead and let's do that. So in order to do that, we have to go to our toolbar component. Let's go all the way to the top here. This is where I want to do it. And let's do const link button like this. And we can already copy, you know, the use editor store here. Like this. And we can prepare some values here. So let's do const value and set value here. And let's make it use state from React. So make sure you have added this import. Editor get attributes link.href or an empty string like this. then const on change here will accept the value which is a string actually let's call this href because this is already called value so we don't accidentally mess it up so editor chain focus extend mark range link set link and then simply set an href and run like that and set value back to zero like this. And now let's actually return the drop down menu. So we can actually copy this highlight color button because the return is fairly simple, right? So let's copy it and paste it here. So the drop down menu is here. Instead of highlighter icon, we will be using link to icon from Lucid React. So just make sure you have added that. And instead of the drop down menu content. It's going to be a little bit different. First of all, the padding will be 2.5, flex, items, center, and gap X of two. And then we're going to have to import the input component from components UI input, and also a button component from components UI button. So just make sure you have added these two here, and I'm just going to move the use state from react all the way here to the top. There we go. Perfect. So we now have these two. So in this input, what we're going to do is we're going to give it a placeholder, which will say search, oh, my apologies, it will be paste link. And then value will be value. On change, we'll simply go ahead and do set value, event target value, like that. And then on the button here, we can add an on click called the on change value and the button will say apply like this or maybe we can do this https example.com right so it's clear what the user has to write inside great now let's go ahead and let's add this link button so i'm gonna go all the way down here and I have to do for the link. So I will just add a link button here. So let's try it out. So normal, let me refresh and let me just add normal text here. I will select this. I have my link here. So if I go HTTPS, google.com and click apply, there we go. This becomes that, right? So what I'm interested in now is can I, like, I can remove this formatting, but just doing this, right? I can undo that back. But is there a way that I can, like, see what is the URL? I kind of have an idea of how we can do that. I'm not entirely sure if it's going to work. So, yeah, this, the reason this does not work is because this component is already rendered. But what we can do is you can call an onclick here. So every time we click to open this, perhaps then we can do set value and use editor, get attributes, link.hrep. Maybe we can do that. So if I select this, okay, looks like it's not working. So let me go ahead and try and console log editor, get attributes, link, .href. So let's see. Is that ever working? It's definitely working here. So odd that it's not transferring inside of here. On click here. Oh, maybe I'm using the wrong on click. Maybe this one. I'm just confirming that I didn't do any typos here. So if I do this. Let's try this again. Let's add something. okay so it looks like it's not working at the moment all right so i found a way you can do that so not on click here not on click here but on the drop down menu on open change we can call the set value here but i think we have to be a And only if it's open, meaning if it's currently rendered. So if open, only then assign the value. And I think that should work. So let me refresh this entire thing. Test. I have a bunch of logs here. We can close this, I think. So I will add HTTPS. Oops, I completely forgot how to write URLs. like this. And if we try and click here, there we go. Now it works. Perfect. Right. And if I go here, for example, nothing. Excellent. So now I can see the exact URL, which is, well, that is inside of my, well, links. Perfect. So that is working. So we've wrapped up the link button. So just to recap, So just add on open change, get the open. And if it's open, set the value to editor get attributes link dot href, right? And this one, honestly, will probably never work. So you can just set it to be an empty string by default. Great. So link button is resolved. So what we have to do next is the image button. That's the next one. So let's go ahead and do that. So, well, let's make these things easier for us. let's copy the entire link button here like this and let's call this image button now what we're going to do is we're going to change this to be image url and set image url like this and we're also going to prepare is dialog open and set is dialog open because we're going to have a model in this case. And let's mark it as false by default. On change here will be a source. And we're going to use chain focus. And in this case, it will be set image source like this and run. Did I add an image? I did. Okay. I forgot that I added that. Perfect. So we have the image here. So this solves if we want to use the input to add our image, if we want to use a URL. But I also want to add the ability to upload. So let's do that here. So I'm going to go ahead and create a const on upload. And we are very simply going to convert it into a local blob. So let's create a hidden input here, which will be document create element, and we're going to call it input. Input type will be a file. Input accept will simply accept anything image related. And then input on change, my apologies, lowercase on change. and we're going to do const file to the event target as html input element dot files question mark first in the array if we manage to grab that file we're going to create an image url here from url dot create object url and pass in the file and then we're going to call on change with our new image URL. So we are practically going to, you know, simulate that we are entering a URL, which is just going to be a local URL object like this. Great. And then what we have to do is actually click on that input. Great. So that is our on upload method here. Great. And let's add one more method before we start building. So const handle image URL submit. If we have image URL, We're going to call on change and pass in the image URL. We're going to pass in set image URL to be reset. And set is dialog open will be set back to false. And now we have to go ahead and actually add our dialogues and all of those other things. So first of all, let's add everything we're going to need from the dialogue. So the same place where we added the drop down menu, We can now add dialogue, dialogue content, dialogue footer, dialogue header, and dialogue title from components UI dialogue, which is another component we got from ChatCN when we added all the components. Great. Oh, looks like it cannot find nameset value. That is okay. We're not going to need this at the moment. Great. So let's just continue, you know, building our image button here. so it will have the drop down menu trigger but this will first of all use the image icon from lucidbex so we are no longer going to be using the link icon and then we're going to offer instead of our drop down menu content here we're going to offer two options so we can remove everything from this class name here and we going to use our drop down menu item which i believe we have not used yet But you know make sure you have it imported here This drop menu item will first offer an upload icon from Lucid React with a class name size4 and MR of 2 in the text of upload And then we're going to have another drop-down menu item, which will have a search icon from Lucid React and a text which will be... paste image URL, I guess. Let's use that, for example. Great. So what we're going to do here is add on click and just call the on upload like this. And here we're going to do on click and call set is dialog open and set it to true like that. And now what I want to do is I want to wrap this entire thing instead of a fragment. So I can render two components here and the other one will be a dialog, right? This dialog will have open of this is dialog open and it's going to have on open change of set is dialog open like that. And then we're going to have a dialog content here, then a dialog header. Inside of here, we're going to have a dialog title, which will very simply say insert image URL like this. outside of the header we're going to create a div with a class name actually we don't need a div at all we can just add our input here with a place folder insert image url like this value will be the current image url from the state above on change here we'll simply set image url with event target value like that and on key down, let's enable kind of like an enter to submit, right? So if event key is enter, handle image URL submit. Like that. There we go. And we're also going to add a dialogue footer outside of dialogue content. And inside, we're just going to add our button with the text insert and on click will do handle image URL submit like that. There we go. And now let's go ahead and let's render our image button all the way down. So we have to do image, image button. There we go. Oh, that is a bad looking button. Let's see how that happened. And so somewhere we are rendering something that we should not be rendering. So dialogue footer here, I think this is the button that's causing issues. But I have to admit, I'm unsure. Oh, the dialogue footer has to be inside of dialogue content like this. There we go. So now you can see that the image looks like a natural buttons here. So I can select to upload something, right? So I'm going to go ahead and try and do that now. So there we go. I chose a random SVG from my file explorer like this. And what we can also do is we can use, you know, URLs. So for example, I think if I go inside of nodes here and find an image, and if I copy, let's use the purple one. If I copy the image address here, and if I attempt to go to the image, paste image URL, and paste it here, there we go. I can do that like this as well. Perfect. So a very cool feature for us to have. You already know that we can just, you know, copy and paste images, but now we can also upload them from our Explorer, and we can also paste image URL. Awesome. So what's left to do now inside of our toolbar here are the align and list. And then we can focus on line height and font size. Great, great job. Let's go ahead and let's develop the align and list component. So let's start with the align one. And let me see, can I copy a very simple component from here? Well, for example, let's copy the link button or maybe even simpler. Let's copy the highlight color button right here. I'm just going to go ahead and add it to the top of my file here. And I'm going to rename this to align button like this. The editor value can stay. The value constant itself will not be needed. Same as the onChange. What will be needed is a new array called alignments, which will be an array of objects. The first one will be alignLeft, and it will have a value of left, and it will have an icon of alignLeftIcon from Lucid React, like this. And then we're going to add a couple more. So we're going to add align center with the value of center and align center icon from Lucid React. And then we're going to have an align right with the value of right and align right icon from Lucid React. And last one, which will be align justify with the value of justify and align justify icon. All of these from Lucid React. Great. So we now have that. The drop-down menu button can stay the same, but we're going to modify this to use align left icon. We already have it imported. And now inside of the drop-down menu content, we're going to iterate over our alignments like this. So alignments.map, and let's destructure the label, the value, and the icon. And we can remap the icon to use the capital I so we can render it. let's open up a button here and let's render the icon and the span and let's render the label inside like this before we go forward let's go ahead and change the class name of the drop down menu content to be padding one plex plex column and gap y of one like that and for the button itself. We're going to give this a key of value. An onclick will be editor, chain, focus, set text align, pass in the value, and run this. And since we have an error, it means that we need the text align extension. So let's see if we can find it here. Perhaps that can be in functionality. Let's see text align right here great so we need this let me just see so just a single one 2.10.2 let's do it 2.10.2 legacy peer depths and there we go and now we just have to add it so text align from extensions align let's go to editor here i'm just going to add it here text align and i will add it right after the starter kit. And there we go. Set text align is no longer throwing any errors. Let's go ahead and give this a class name here. Let's open up CN and let's add flex items center gap X of two, PX of two, PY of one, rounded small, hover background neutral 200 with an 80% opacity. And if editor is active when it comes to this specific text align, so passing the value, in that case, we're going to show background neutral 200 with an 80% opacity. Let's give this icon a class name of size 4. And let's give the span a class name of text small, like this. let's go ahead and let's use the align button here align button like that and let's try it out i'm going to go inside of here i can already see my button so if i select some text and click here looks like i'm getting an error that's because i have to refresh this here so let me remove everything here can i remove the table i can great test let's go ahead and select this looks like it's not working. So am I getting any errors? I'm not getting any errors. So let's go ahead and let's debug why this is not working. So align button here. Did I add an on click? I think I did add an on click here. So it certainly should be working, but perhaps there is an issue of sorts. I'm going to pause the video and try and debug this. So I think that the issue is inside of the editor where we added the text align, what we're missing is a config here. So text align needs specific types and this is configure, not config. So types, let's add heading and paragraph like this. And I think that now it will work. So if I go ahead and add a test here and select this, there we go. I can now select and align my text in different ways. Perfect. So what's next on the line to do? Let's see. I believe it is a list component. So let's go ahead and let's wrap this up with the list component. So what I'm going to do is I'm going to copy the align button here. So I'm going to copy the entire thing like this. I'm going to paste it here. And I'm going to call this line list button like that. The editor can stay the same. Instead of having constant alignments, We're going to have a constant called lists because we're going to have two types of lists inside. The first one will be a bullet list and the icon will be list icon from Lucid React. Is active will very simply be if editor is active, is bullet list. Okay.\nAnd on click here will be editor chain focus toggle bullet list dot run like this. And then we can go ahead and copy this and make this an ordered list. Use this list ordered icon. icon. This will be ordered list, and this will be toggle ordered list like that. There we go. In regards to the drop down menu, the icon we're going to be using is the list icon. We already have that from Lucid React. Inside of here, we're going to iterate our lists like that. We are not getting the value. We are getting the icon and we're getting some new things like on click and is active. So the key can be the label on click can be on click. And we can just do is active here. And we probably should have done this for all of our, yeah, just make sure to execute is active because it's a function. We should have probably done this way of maps for all of our components, but yeah, here we are. Okay. And this can stay the same. Great. And I think that's it. I think we did it. So if I go back now and if I change this list to use list button, I think that it should work. So I'm going to go ahead and just go down here, test and if i click here bullet list or ordered list it is working flawlessly amazing amazing job now let's go ahead and let's build the font size functionality there is one issue though So font size does not currently exist as an extension inside of TipTap, which gives us a perfect opportunity to learn how to build a completely new extension or how to even extend existing extensions. So let's go ahead and do that. So I'm going to go ahead and instead of my source folder, I'm going to create a new folder called extensions like this. Keep in mind that inside of here, I will not be creating any components. I will just be writing extensions for TipTap. This is important to mention because inside of my Tailwind config, that folder is not included for Tailwind. So it's not suitable to build any components. And same goes with the lib folder and the store folder. So my extensions will be reserved for TipTap extensions. Inside of here, I'm going to create font-size.ts like this. Let's go ahead and let's import extension from TipTap React. And let's import at tipTap slash extension dash text dash style like this. Let's declare module at tiptap slash core. And let's create an interface. Commands, return type, font size, set font size, size to be a string and return type. Then let's also add unset font size to just be return type. like this. Then let's export const font size extension, which will be extension.create. Let's give it a name of font size. Then let's go ahead and let's add options. And the options will simply return types, text style. and i it looks like i cannot add a comma here oh yeah because this is returning my apologies yeah after that let's go ahead and let's do add global attributes and let's return an array and let's return an object inside of this array which will be types this options types attributes, which will be font size, which will have a default of null. Parse HTML will take the element. So this will be a short arrow function. Element, element.style.fontsize. Then render HTML will again take attributes this time. If there are no attributes font-size, in that case, simply return back an empty object. Otherwise, go ahead and return a style. Go ahead and open backdix font-size and render attributes.font-size inside. Like that. There we go. Then, after the global attributes, go ahead and do add commands. Inside of here, return an object. The object will have set font size, which we will finally use in our toolbar, which will accept font size, which will be a string. Go ahead and open another function and destructure chain from the props. return and execute the chain method and chain set mark text style font size like this and then run this like this and let's do the same thing for unset font size like this chain and return chain set mark text style font size null remove empty text style and run there we go so we have developed our very first custom extension for tip tap and we've also added the proper types, which we are going to be able to call inside of our toolbar. So what we have to do now is we have to go inside of the editor and we have to use the font size extension. So let's go inside of our editor and let's import the extension. So I'm going to separate that from the ones above because this is a custom one. So font size extension from extensions, font size. And I will simply add it after my starter kit like this. There we go. Font size extension. But we're not done yet. We now have to go back inside of our toolbar and we have to develop the actual font size button component. So let's go ahead and let's do that. So we can already prepare by simply copying the last one, which is the list button. So let's copy this. Let's paste it and let's rename this to font size button. So this time we will not be using any lists, so we can remove this. And we're actually not going to be using any dropdowns either. So what I'm just going to do is I'm going to return just a div like this. And we're going to style this later. So I will add font size as a text here. So yeah, I didn't really copy much. But let's immediately add this here. So to do font size, font size button, and I should now see the font size text right here. Great. Let's go ahead and let's continue developing inside of the font size button. So let's add all the functions which we are going to need here. So const current font size will be editor, get attributes, text style, and get the current font size. Now, if the current font size exists, in that case, what I immediately want to do is strip out. So for example, you will have like 70px. For the current font size here, I don't want to render px. I just want to render 70. So what I'm going to do is editor, get attributes, text style. So basically the exact same thing I'm doing in the line above here, dot font size, dot replace. px with nothing like this else fall back to 16 like this so this is all for one constant a ternary here now let's go ahead and let's add some states so font size set font size use state current font size we're going to start with that then input value will be set input value use state and we can start with font size. Let me see. Yes. So font size will be exactly the same as states. Right now, it seems confusing. Why do we even have this many states, including this one is editing and set is editing, but I'm going to explain in a moment. So this will be quite a different component than the ones we are used to. So we're going to have a minus. Okay. So we're here. We're going to have like a minus button, a plus button. And in between, we're going to have an input, which will showcase the current value. But we will be able to type inside of that input and change it to any value we want. But we have to be careful with that because we also have to display the current font size. So let's define our const update font size method, which will get the new size, which will be a string. Let's go ahead and convert it into a number using parse int like this, new size. then what we're going to do is if not is not a number size and if size is larger than zero so if everything's okay with the size what we're going to do is editor chain focus set font not font family set font size you can see how it recognizes it right because of our font size extension If yours is not recognizing it make sure that you didn do anything wrong here and make sure that you have added your font size extension here I going to try and change it So if I change this to this there we go You can see how I get an error right So this declaration module is very important here. So set font size. I'm going to open back text and I'm going to add the size and then px.run like this. There we go. After that, I'm going to call set font size and I'm going to call that the new size. So this one, right? Not this one. This is optimized for the chain. Then set input value, which will be new size. And set is editing to be false. Like that. Then let's do const handle input change, which will take the event, which is a react change event. HTML input element. and very simply set input value to event target value. Then let's go ahead and let's implement const handle input blur, which will be update font size input value. So this will kind of be an unexplicit submit. If we blur the input, we're going to assume that the user has finished typing their size, and it's going to be the same thing as if they've pressed the enter. And now we're actually going to handle the enter. So handle key down. Again, we need this react keyboard event. HTML input element. If event.key is enter with capital E, we are going to prevent default here. And we're going to update font size to input value. And editor commands focus. So we focus back on the editor like this. Great. Just a couple more of these. So const increment will be const new size parse int font size plus one. And then update font size to new size to string. Like this. and then we can copy and paste this and do a very similar one for decrement like this. So this will be minus one and we are going to ensure that new size is above zero. So we don't go into minus with our size. So those are going to call the update font size, which we'll call our new extension here and also handle all the other visual effects that it needs. So we are going to be using the font size for one thing and input value for when the user is actually typing. You're going to see us use these different values which we have stored now. So inside of this div here, go ahead and give it a class name, flex items center and gap of 0.5. Now let's create a first button which will use the minus icon from Lucid React. Let's give this minus icon a size of 4. So there we go. We now have a minus icon. What I want to do now is save us some time and just copy any button class name. So I'm using the one from the list button. So just copy the class name because all of them are the same, right? So I'm just going to go ahead and copy this and paste it here as the class name. There we go. So let me just see, can I modify somehow? I will remove the min and just make sure it's a height 7 and width 7 like this. No need for flex call here. Justify center can stay and hover can stay. PX and overflow needed. All of these things are not needed here. We can remove it. There we go. So a bit simpler. Great. So that is here. And there we go. Now it looks better. So we also have to do on click here decrement. like that. After this button, we're going to check if is editing. We're going to render a native input. Otherwise, if we are not editing, we're going to add another native button like this. This button will have the same class name here. So let me just copy it, but it will be slightly different, but most of it, I think it's going to be the same. So the first thing we're going to changes the height the height of this button uh my badge is the width the width of this button will be 10 we're also going to ensure that it has text small and i remove the shrink if you didn't see i don't think we're going to need it uh flex is not going to be needed item center is not going to be needed. Neither is the justify center. And let's go ahead and also add a border here. And let's add a border neutral 400, like this. And let's also add text center here. There we go. Now, inside of this button, I'm going to add a current font size. And let's give this button on click here, set is editing to be true. And let's update the font size to current font size. So right now, there we go. You can see it says 16. And when I click here, we switch to an input, right? So by default, this will just be a preview, right? It looks like an input. So the user knows that they can click here and edit, but actually it's just a button. So now let's go ahead and let's actually add the input styles here. So I'm going to give it a type of text. I'm going to give it the value of input value. I'm going to give it an on change to be handle input change. I'm going to give it on blur to be handle input blur. I'm going to give it on key down to be handle key down, like this. And the class name I can luckily copy from the button below like this. So very similarly, the input is going to look like this. A few things we're going to change is we're going to remove the hover. So no need for that. And instead, we're going to add a background to be transparent. And then we're going to add a focus outline none and focus ring zero. So we added that to our input here. So now if I click here, you can see how you almost can't notice that it has turned into an input now, right? Great. And we can actually make this even better. So we can turn this button and remove the hover like this. Keep the background transparent just as we do in the input above and use the cursor text so now it looks like it's already you know something you can write in and i think that's really cool right but when you actually unfocus it turns back into a button really cool uh great so now let's go ahead and let's copy this button here and just paste it after this whole ternary here so this will be increment and let's use the plus icon from lucid react and let's finally try this out so test here there we go you can see how i can increase i can decrease or i can write 16 and press enter or i can for example write 28 and blur and it will stay 28 amazing so go ahead and choose how you like it for this, I use this trick, you know, I gave it a cursor text and I styled it exactly as my input. So the user almost won't notice that it's actually not an input right now. It's only an input when you double click on it. Why did I even do that? Well, because remember, I needed to display the current font size. So if I have, you can see now it's 22, but if I go down, it's 16, right? You can't exactly update that in an input, which is controlled by the current value, right? You can do it with the use effect, but then you are just having a conflict of multiple values. So this is a kind of a tricky way to do it. Yeah. So what you have to do is you have to, you know, double click on here. If you want to make it more obvious, you can reset to what we had before, which is basically having a hover VG neutral 200 with an 80% opacity. Did I add this on the correct thing? I think it did. So there we go. I just had to refresh. So if you want to, you can turn it like this. Yes. There we go. Perfect. So that is it for the font size. You can see that it's working perfectly. So we are modifying the font size style attribute here. We added all of this amazing commands here. And we even added this one, which we are not even using. But yes, you can explicitly remove font size if you don't want to use it anymore. Perfect. So I'm very happy with how this turned out. So what we have to do next, which is the last one on the list, and that is the line height, right? So another exercise for us because the line height does not exist in the current extension with the current extensions of TipTap. So we're going to learn how to do that as well so we can have some cool features and say that our Google Docs or Microsoft Office Word clone is different than all the others available. Amazing, amazing job. Just as we've created our custom font size extension, let's do the same thing for the line height. So I'm going to go back inside of my source extensions, and I'm going to create a new file, line height.ds, like this. We can go ahead and copy the extension import here. So we save some time and we can also copy the declare module here, but we're going to slightly modify it. So this will be called line height and we're going to have a set line height. This will be a line height like this and we going to do unset line height like that Then let do export const line height extension to be extension Create. Let's give it a name of line height like that. Let's give it add options here. inside of here we're going to return the types to be paragraph and heading and then default line height will be normal keep in mind that we don't have any type enforcements for this so be mindful of any typos here great so after we've added the options let's add global attributes here so let's return an array of objects like this. And we're going to return, well, pretty similar to what we did in the font size. So types will be this options types. And then attributes will be line height. Default, this options default line height. Render HTML will take the attributes. and if there are no attributes dot line height we're going to return back an empty object otherwise we're going to return back a style open template literals and write line dash height attributes line height again be mindful of any typos we don't have type safety here Now, after render HTML, let's do parse HTML, which takes the element and simply returns either element.style.lineHeight or this options default lineHeight, like that. After add global attributes, let's do add commands. Inside of here, return an object. The first one will be setLineHeight. We're going to acceptLineHeight, which is a string. We're going to open another method, which will have TR, which stands for transaction, state, and dispatch. And now, from the state, what we can do is we can destructure our selection. and then tr can be tr.selection, my apologies, set selection and pass in the selection. From the selection, we can destructure from and to. And now we can capture all nodes we need to apply the line height to. So state document nodes between from two. And the third argument is a function which has node and position as the arguments. If this options types includes node type name, in that case, set the transaction to be transaction. the set node markup position as the first argument, undefined as the second argument, and an object as the third argument. Inside of this object, we're going to spread node attributes, and we're going to add our line height like this. So this is quite confusing, I must admit. You can, of course, the more of these you build, the easier they're going to become. one piece of advice, don't be shy with asking AI to do this for you. I personally use Cursor to develop apps and it was so helpful for me to, you know, resolve some problems, right? Some things I didn't understand. And once you build one working extension, you can always give it to Cursor as inspiration for the next one, right? So one working extension is pretty much all you need. And you can ask cursor or any other AI to just go wild and create anything else. Great. So where were we? We have set this transaction here. So now we can go here outside. And if we have dispatch, because it's optional, we call the dispatch with that transaction like this. and let's return true like this. Great. So one more thing alongside set line height. Let's also add unset line height like this. And again, transaction, state, and dispatch. Destructure the selection from state. Set the transaction to be transaction. setSelection to the current selection from the state. The structure from and to from the current selection. Run this over in between the from and to values. So state document nodes between from to and the third argument is a method with node and position as the arguments. If this option types includes node type name, set the transaction to be transaction, set node markup position, undefined, spread the node attributes, and set the line height, and fix this, undefined, and we have to set the line height to be this, options, and we can again use this one, the default line height like that. There we go. Now, if dispatch, dispatch the transaction and return true. There we go. No more errors. So yeah, this one is pretty much the same as the one above. As you can see, even this line is the same. I believe that even this line is the same. So we could have copied a lot of things. The only thing that matters is, well, the line height property, which we have changed here. There we go. So we have developed the line height custom extension, and now we have to add it and import it. So I'm going to go inside of my editor here, and I'm going to import line height extension from extensions line height, and I have to edit now right here, line height extension, and I also have to configure it with types. So let's add heading and paragraph. I think that that is correct. Yes. There we go. So we have that now. and now what we have to do oh yeah we can also pass in the default align height but I'm just showing you the options you can pass here right so you can change those things but we also defined those in the beginning right but you can also explicitly add them here as well great so what we're going to do next is we're actually going to head and use the line height extension inside of our toolbar. So let's go ahead and let's implement the line height button. So for that, we have to revisit our toolbar component here. And let's go ahead and let's copy. How about we copy the align button? Because I think this one will actually be the most similar. so I'm going to go to the top now above my font size button and now I have this double align button I'm just going to rename it to line height button so it's going to be similar not going to be as complex as the font size one certainly so instead of having alignments inside I will be having line heights inside so I'm going to go ahead and select the label here and make this default with a value of normal like this. And then I will have a label single with a value of 1, then a 1.15, which will be 1.15, then 1.5 with a value of 1.5. And then we're going to have two with a label of double like this. There we go. The drop-down menu can stay the same except the icon, which will be list collapse icon from Lucid React. Instead of iterating over alignments, we are iterating over line heights this time. The key value can stay the same. And instead of set text align, it's going to be set line height here. And there will be no icon destructuring in this one. And for the is active, we can do get attributes. And we can do paragraph dot line height is equal to the current value. That's how we can detect whether the current one we are covering over is active, right? And we can remove the icon here. And I think that should be it. Let's go ahead and let's use the line height button here. So let me just add it to our last to do here. There we go. So what I'm going to do now is refresh. I'm just going to remove everything. Just go and remove this. Okay, so test, one, two, three. I'm adding all of these lines here, and I will select all of them, and I will attempt to select 1.15, 1.5, double, nothing is working. Okay, so now I'm going to debug. Why exactly is that happening? what exactly did I do incorrectly. All right, so I found the culprit, and it's because of my line height extension configure here where I misspelled paragraph. So now I going to refresh remove this test test test test I going to select all of this and I will attempt to change this And there we go We can change this right now Perfect. And I also want to see, does this work if I don't add anything here? Do I have to do that? So let's try this again. Test, test, test, test. Because that's what actually messed me up. Looks like we don't need to add anything. it will work just fine. And you can see how it can also recognize where we currently are. Amazing. So we just added multiple custom extensions and we pretty much wrapped up the toolbar. The only thing that's left with the toolbar, I believe if I search for to do here is the comment functionality. We're going to do that later when we add collaboration, but yeah, Everything else is implemented. Perfect. So our toolbar is finished. What I want to work on next is implementing the ruler so that we can add some margins to our file. And then we're going to slowly start to style this into a proper navbar because right now this toolbar is just randomly here. But we're going to style it so it's a little bit better and makes more sense. Amazing, amazing job. Now let's go ahead and let's create a ruler, which is going to stand just above our document. The ruler will be used to change the margins of the paper using two markers. Let's go ahead and first develop a static ruler right here, which will be the exact same width as our paper. So we're going to do this inside of the editor component here. So let's go inside of documents, document ID, editor.tsx. And inside of here, what we're going to do is we're going to go above this div encapsulating the editor content like this. And we're going to add our ruler like this. And then in the same folder, we're going to create a new file ruler.tsx. And let's go ahead and export const ruler here. And let's return the ruler. Then we can go back inside of the editor component. and we can import ruler from dot slash ruler. So just confirm you've added the correct import here. And then when I refresh this page, I should be seeing a ruler text right here. Ensure that you have your app running. And let's actually fix this error here. It's a tip tap error. Server side rendering has been detected. Please set immediately render explicitly to false to avoid hydration mismatches. So we go inside of the editor. instead of use editor here. And I think that we can just add it here like this. Let's see. Yes, we can do that here. So now I'm going to go ahead and add some empty space here and refresh and see, there we go. No more errors are appearing. We resolved that as well. Great. Let's continue developing our ruler component here. So the ruler component itself will have a div with a class name, height of six, border, bottom, border gray, 300 like this, flex, items, end, relative, select none, and print hidden, meaning that I don't want the ruler to be shown when we are actually printing this paper. And then we're going to go ahead and create another div inside. We're going to give this div an ID of ruler dash container, and we're going to use this later. And let's also give this a class name of maximum width of 816 pixels. Why 816 pixels? Well, because that is the width of our white paper right here, right? So we want this to be the same width. So maximum width of 816 pixels, MX auto, so it's a centered, full width, full height, and relative, like this. And now, let's go ahead and let's actually develop the, well, like the ticks on a ruler, right? That's what we have to do next. And for that, I just want to quickly define my markers. So const markers on a ruler will be array from, and we're simply going to do length 83. So if you're wondering why length 83, well, I found this to be most compatible with 816 pixels width. You're going to see why 83 in a moment, but basically it was a bit of trial and error for my part. I basically want each tick here. So we're going to have a bunch of ticks here. And with 83, they completely fill up the filament here, right? So that's why 83. And let's just go ahead and immediately run a map function here like this. So we just return the marker number. Great. So we now have the markers here. So let's go ahead inside of this div here. create a new div and give this a class name of absolute inset x0 bottom zero and full height and then another div with a class name of relative full height and let's give this a style actually no need to give it a style because it's not going to be dynamic so we can just go ahead And once again, do 816 pixels here. And then let's iterate over our markers. So markers.map, get the individual marker like this. And first, let's go ahead and let's get the position of each marker. So we can get the position of marker by getting the current marker number, multiply it by 816, which is the full length of our paper, and divide it by 82 like this. And now that we have the position, we can go ahead and return an actual div element. So this will be another div, which will have a key value of marker. It will have a class name of absolute and bottom zero. It will have a style of left. And then we're going to open back this position px, right? So we don't want to do that here. We don't want to do left and then, you know, open template laterals inside and do it like this. Because again, Tailwind has a just-in-time compiler, which does not support this kind of things. It can work, but it's not really reliable, right? So it will only work if somewhere else you have already defined all of those class names, right? So Tailwind does not generate class names that are not used. So you cannot dynamically add class names, which you think might be used in the future, right? So that's why it's safer to do these kinds of things instead of a style element. Great. So we have this now. And now let's do a couple of modulus operations here. So marker mod 10 is equal to zero. So every 10th, right, is going to be the following. A div. My apologies. This will be a self-closing div. with the class name absolute bottom zero width of one pixel height of two and the background of neutral 500 like this and let's see there we go you can see how we already have some ticks ready here great uh so we have that and now below that let's add a span element here marker divided by 10 plus one like this there we go one two three four five six seven eight nine like that let's give this a class name of absolute bottom two text 10 pixels text neutral 500 transform and minus translate minus x one and a half like this there we go so now they'll align on their ticks like that Great. So now that we have this, let's go ahead and add some other positions of the markers. So marker mod 5 is equal to 0 and marker mod 10 is not equal to 0. So we don't want to repeat this, right? So we are only targeting this ones. But, you know, if those that have a mod 5, 0 can also have a mod 10, 0. So we have to exclude those from here. So then let's go ahead and do a very similar thing. So this one will only display a tick. They won't display any text here. So class name here will be absolute bottom 0, width of 1 pixel, height of 1.5, and the background neutral 500 like this. There we go. You can see how now we have this in-between text here. And let's do one more here. So any other marker, which is basically means if marker modulus 5 is not equal to 0. So those are all the other positions. In that case, again, we're going to have a self-closing div here. And we can actually copy this entire thing like this and just reduce the height to the height one instead of height 1.5. And there we go. Now we have a beautiful ruler right here at the top. So you can see how we used modulus operator here so that we get, you know, every fifth, every tenth, right? So we can display these larger numbers here. And we filled up the entirety of our space here, right? So basically, I played around with the length of these arrays. And you can go ahead and try some solutions of your own. But you can see if I add like 83 here, then I'm missing the last tick here. So that's why I put 82 here. There's definitely proper math between this. So you might be laughing at me right now. But yeah, if you know a better solution, you are, of course, free to implement it yourself. But this is quite a nice way to get a ruler here. Okay, so now what I want to do is I want to actually add blue markers, which we can kind of drag around so we kind of control the padding of our, well, application. I mean our paper, right? So let's go ahead and let's implement a marker component. So I'm going to create an interface, marker props like this. I'm going to give it a position of number. I'm going to give it an is left of Boolean. I'm going to give it an is dragging of Boolean. on mouse down to be a void. And I'm going to give it on double click. Oops, I used wrong.\ncharacter here. There we go. So those are the props of our marker. And then let's define the marker. So marker here will have marker props like this. And we can go ahead and destructure all of those. So position is left, is dragging on mouse down and on double click like this. And now let's go ahead and let's add a div here. Let's give it a class name of absolute top zero, width of four, height of full, cursor, EW resize, Z index of five, group, and minus ML two, like this. Then let's go ahead and add a style element here. So we're going to dynamically decide which side we are going to push using the isLeft property here. So open square brackets and check if isLeft. In that case, the key we want to move is left. Otherwise, it's going to be right and the value will be open backticks. And we're going to use our position in pixels like this, right? So this is the same thing as adding left or right. But since we don't know which one it will be, we need to make it dynamic. And you do the dynamic key inside of objects in JavaScript by adding square brackets like this. So then you can do dynamic operations inside. Great. Then let's do on mouse down here to do on mouse down. And let's do on double click here to be on double click. There we go. Now what we have to put inside is a caret down icon. And while you can definitely find carrots from Lucid React, I found it much better to have them in React icons. So let's do npm install React icons. And if you're interested in which version I am using, let me just show you. So at 5.3.0 dash dash legacy peer depths. So again, at the moment I'm recording the video, React 19 is a release candidate. So I have to add this flag. If you're watching in the future and the React 19 is stable and completely out, you most likely won't have to add this here. There we go. So we now have React icons. And now I'm going to import that exact icon which I need. So let me just go here to the top. And I'm going to import the FA caret down from React icons FA like this. And then inside, we can render the FA carat down, and it's going to be a self-closing element like this. Now let's give it a couple of class names here. Absolute, left, one and a half, top zero, height full, fill, blue, 500, transform, and minus translate, X, one and a half, like this. So those are the class names for FA carat down, like this. And I think we can already render this marker just so we can see what we are developing, right? So inside of the div where we added an ID for the ruler container, go ahead and add a marker here like this. We're going to give it a position here of 56. We're going to give it an is left of true. We're going to give it is dragging to be false. We're going to give it on mouse down to just be an empty arrow function. And on double click again to just be an empty arrow function. So it says that marker cannot be used as a JSX component. And that's because inside of my marker here, I forgot to wrap this entire div inside of a return. So make sure you are doing that. So I think that right now, if I go back, there we go. You can see that I have my marker here, right? And I also have this cursor, which indicates that I should be able to drag this left and right. And my position of 56, which I have passed here, put it in this spot right here. And you can see that it actually matches exactly my padding, right? So if I go inside of my editor here, if you remember, I added this style of padding left 56 and padding right 56. That is because that will be our initial position for the margin. So later, when we make this dynamic and we allow this marker to move, that's going to update this padding left or padding right to its proper position. So that's why I put 56 here for now. And then you can copy and paste this marker like this. Change the is left to be false and also leave this at 56. And there we go. We now have two of these markers right here. Great. So what we have to do now is we have to implement some kind of logic to actually move these markers around. The truth is that we will heavily modify this later. Well, not heavily, but we will modify the way we store the value of the left and right position because all of this will need to be collaborative, right? So we will have to find a way to share this state with the other users on this page right here, right? We want them to see the ruler changes as well. So what I'm going to do is I'm for now just going to create this locally so we can see what we're doing. So I'm going to set left margin and set left margin here. Use state and we can start here at 56. Then I'm going to copy and paste this and I'm going to call this right margin and I will just add capital letter here like that. Then let's also define some more states. So is dragging left and set is dragging left. Use state and false. And then we're going to do the same thing for right. Like this. There we go. And then let's add a ruler ref from use ref from react. And let's give this an HTML div element. And by default, it's going to be null. So just make sure you have imported a useRef and useState from React here. And then let's go ahead and let's develop const handle left mouse down, which we'll call setIsDraggingLeftToTrue. Then we can copy and paste this. And we can do the same thing for right mouse down and setIsDraggingRightSetToTrue. Great. So now what we have to do is we have to develop the logic on actually moving these markers around. But there are a couple of constraints. So first of all, we have to develop that they actually follow the cursor on the X axis, right? We must not make it so that the cursor will be followed upwards or downwards, right? So only on the X axis. The second thing we have to do is we have to ensure that the left margin can never go above right margin. And we also have to ensure the same thing for the opposite. And we also have to ensure that between these two, there's always a little bit of space available. We don't want to crush all our text into one thin line, right? So let's develop that. Const handle mouse move here. we'll take an event which will be react mouse event here and now let's go ahead and start developing this so first of all we're going to open an if clause here and i'm going to open another parenthesis inside and i'm going to check if is dragging left or if is dragging right and if we have ruler ref dot current right so only then are we actually going to do this First, we get the container using ruler ref dot current query selector like this. And we're going to add the ruler dash container like this. Right. So that's why we added the ID for the ruler container here. And while we are here, let's also just do a couple of quick things before we continue developing the handle mouse move. So it actually makes more sense. on our most top div, we're going to add a ref of ruler ref. We're going to add on mouse move to be handle mouse move like this. And we're going to prepare on mouse up here. We don't have on mouse up at the moment, I believe. And on mouse leave, we're also going to prepare for them to be arrow functions like this. And now let's continue developing the handle mouse move method here. So now that we get the ruler container via ID here, so just make sure you didn't misspell this, then we're going to check if we have the container. Let's get the container rect. So container get bounding client rect. Let's get the relative X position. So E dot client X minus container rect dot left like this. Const raw position will be math max zero and math min 816 pixels and relative x like this. So we are adding some constraints of the maximum value it can have and the minimum value it can have, right? And then we're going to do if is dragging left. In that case, we're going to do maximum left position that we can drag this to will be 816, which is the length of our paper. Minus right margin minus 100 like this. So now let's go ahead and do const new left position to be math.min raw position and max left position. So whichever one is smaller. And then we're going to do change left margin or set left margin to be new left position like this. And I'm going to add a comment here to do make collaborative like this. And then we're going to do else if is dragging right. In that case, we're going to do similar things. So, const max right position here will be 816 minus, open parenthesis, left margin plus 100 like this. Const new right position will be math max 816 minus raw position and 0 like this. And then const constrain right position will be math min new right position and max right position So we just adding constraints So those edge cases cannot happen where one of these goes above another. And we also want to ensure that they always have a little space between them, right? We don't want to be able to join them together. And then set right margin here. We'll call constraint right position like that. There we go. So now let's go ahead and let's wrap this up by adding const handle mouse up here. And a set is dragging left to be false. Set is dragging right to be false like this. So that resolves. Oops, I scroll up. Okay, so handle mouse up. And we have to do const handle left double. Actually, we can do. Yeah, so let's do individually. handle left double click so we want to add the functionality to reset these markers to their initial position and we can do that by double clicking on them right so if we double click on a marker we're going to do set uh left mark left margin 56 and then we can copy and paste this and change uh this one and the right double click to change the right margin there we go so now let's go ahead and let's append on mouse up here to be handle mouse up let's go ahead and add handle mouse up to be on mouse leave as well so both of them will call this method right here and then for our markers for the position here we can add left margin for the first one, which has the is left set to true. Is dragging will be is dragging left. On the mouse down will be handle left mouse down. And we're going to have handle left double click here. Right. And then we can do the same thing here. So right margin, which, and it also needs have is left set to false is dragging right here then this will be handle right mouse down and this will be handle right double click like this so the only thing we are not using at the moment is actually the is dragging prop in the marker but we are going to add it in the moment i just want to see if this is working so there we go you can see that i can now drag my elements around and as you can see, there's always some space between them, right? So I can't go above them, right? There's always going to be some space left here. And that is thanks to this function here, which we have developed called handle mouse move, right? So we have this 100 offset, right? So these are kind of magic numbers at the moment. So if you want to, you can kind of add constants, for example, page width can be 816. Like this, you can replace all instances of 816 with page width. And then you can do const minimum space and make that 100. And then replace all instances of 100 with that. And then it will make more sense. These numbers themselves will make more sense. You can see that now the minimum space between them is 100. it, right? So if I change this to 50, I have set it in half, right? So you can see how they can get much closer now. So you can decide for yourself, how close do you want this to be able to go near one another? Great. So we are almost done. One more thing we have to do is we have to display a nice blue line to indicate exactly how these margins are moving. And we can do that quite easily we have everything ready we just have to use this is dragging prop in our marker so let's go after the fa carat down here and let's add a div element which will be a self-closing tag let's give it a class name absolute a left one and a half top four transform minus translate x one and half, transition, opacity, duration, 150. And then let's go ahead and add some styles here. So what I'm going to do is actually set, well, we can actually write all of this in Tailwind as well, but let's do it like this. Let's just do style for now. So height is going to be 100 VH, width will be 1 pixel here. Transform will be scale x 0.5. Background color will be 3b82f6, like this. And display will check if is dragging, in that case, block, otherwise none. like this. And we can actually remove the transition opacity because nothing will be controlled with that. And we can remove the duration as well. So now if I refresh, there we go. You can see the nice line, which now follows our app, follows our marker as we drag around. So this will enable, you know, the user to see more clearly where their content will go. Great. So we have developed that. What we're going to have to do next is, well, actually style this a bit better and put it in a proper navbar. Nevertheless, great, great job. Now let's go ahead and let's build the proper navbar layout here. In order to start that, we need to obtain a logo. For the logo of your app, you can use any image you want. if you want to find some cool ones, you can visit logoipsum.com, which is like a place to find placeholder logos, right? So I always use these in my projects. I think they're really cool. So logoipsum.com and you can just find any logo you want, click on copy, and then inside of your project, go and find the public folder. If for any reason you don't have one, you can just create it. it's outside of the source like this. So public folder and create a new file, logo.svg. And since you have just, you know, copied by clicking, you can just paste it inside and that's it. That's the logo. Or you can literally download an SVG, right? Whatever you prefer. And now that you have the logo inside of your public folder here, let's go ahead and let's add a navbar component. So that's inside of app documents, document ID here. And I'm going to add a navbar.tsx like this. So let me just export const navbar. And I will return a navigation element here. And let me add an image from next image. And let's use that source logo SVG. So no need to add a public prefix here because they are automatically public. They're automatically at the root of our app. Let's add an alt of logo like this. So now that we have the navbar, let's go inside of our document ID page and let's put the navbar above the toolbar and make sure you import it from dot slash navbar. And let's go ahead. Oh yeah, I did not add any width or height here. So let's do that. So the width of my logo will be 36 and the height will be 36 as well. And there we go. We now have a nice logo here in the utmost left corner of our app. So now let's go ahead and properly style this. So I'm going to give this nav a class name of flex items center and justify between. Like that. Then I'm going to go ahead and wrap a div around my image here like this. And I will give this a class name of flex, gap2, and items center. And then I will wrap my image inside of a next link. So just go ahead and wrap the image around that. And then give this an href to go to the root page. So now when you click on this logo, you will be redirected to the root page here. And then you can use this to go back to slash documents one, two, three. Great. So we have that now. And now outside of the link, create a new div with a class name of flex and flex column. So now in here, we're going to develop a document input. And here we're going to have a menu bar, right? So they're going to be one below another, but they're both going to be kind of like next to the logo, right? So let's start with the document input here. We are later going to make this even more functional, but for now, it's just going to be a placeholder. So document-input.tsx like this. Let's export const document input. Let's return a div with a class name flex items center and gap of two. Then we're going to go ahead and display a span. And we're just going to say untitled document like this. Let's give this span a class name of text large, px of 1.5, cursor, pointer, and truncate, like this. And then after this span, we're going to go ahead and simply render a bsCloud check icon. And we can import bsCloud check from React icons, which we installed in the previous chapter, I believe. so from react-icons bsbs cloud check like this there we go and now we can go inside of the navbar and you can actually render the document input from dot slash document input so there we go now you see we have a untitled document text here and we have a nice cloud indicator like everything is saved, right? Everything is fine. So that is it for the document input. We are later going to go back inside and develop it. What we can do now is we can develop our menu bar. So in order to do that, we're going to have to import some things from Shatsy and UI. So from at components UI menu bar, import the menu bar itself, the content, item, menu, separator, shortcut, sub, subcontent, Sub Trigger and Menu Bar Trigger You should have all of this available from the Menu bar inside of your source components UI menu bar Great So we have this and now let go ahead and let actually develop it We are again not going to add all functionalities immediately, but we will add more and more of them as we go along. So let's give this a class name of flex like this and let's add a menu bar here. the menu bar itself will have a class name of border none background transparent shadow none height auto and padding of zero and then inside of here we can add a menu bar menu like this and we can add a menu bar trigger and we can add file for example so now you can see that we have like little menu here and we have a text which says file and then we can add a menu bar content here and inside of here we can for example add a menu well how about we do just the menu bar item and just write save and for example we can have a file icon from lucid react and give this a class name of size 4 and mr of 2 So I have imported file icon from Lucid React. I will just move it here like that. And then we go. Now when you click on file, you can see you have a nice little save pop-up here. So that's how the menu will work. So I'm going to stop developing here. Of course, we're going to have some more menus and more options. But for now, this is just fine. And then what I want to do is kind of end it here for the nav bar. We can go back inside, but now I want to go inside of page.tsx right here. And let's actually style this a bit better. So we're going to wrap both the navbar and the toolbar inside of their own div here. And let's give this a class name of flex, flex column, px of four, padding top of two. Gap between those two will be two. It will be fixed. Top will be zero. Left will be zero. Right will be zero. Z index will be 10. Background color will be FAFBFD, the same one as above. And we're going to have print hidden. So this is not visible when printing, right? And then we're going to encapsulate our editor into its own div as well. Because now that we set this as a fixed position, what happens is that the editor now goes kind of behind our navbar and toolbar. So we have to push that. So let's add a class name here, padding top 140 pixels. And on print, padding top will be zero. Why? Well, because this entire thing will not exist on print. And there we go. Now it looks much better. So how did I know that we exactly have to put the padding top of 114 here? Well, that's because when you hover over this thing, there we go. You can see approximately it's 112. So I just added a few more pixels here. If you want, you can be very specific, so you can kind of fix the height if you want to 112 pixels like this, and then you don't have to worry about this content ever going behind. But we're going to make sure that on small devices, this doesn't get collapsed, so it never expands the height, right? So it always looks good. But you can already try and zoom out, zoom in. You can see that it's very hard for this to break into multiple columns, right? So we can already test out mobile just a little bit. And there we go. So we can see that we can scroll here. Creating a responsive document can pose quite a challenge. And while there are definitely ways that we can kind of do this, even Google Docs and Microsoft Word give you an actual native application. They actually block you from visiting a Google Docs on mobile, right? So that's why we're not going to focus too much on creating something that even Google wasn't able to do. But we will, you know, take care that it doesn't look broken, right? Great. So now that we have this, let's go ahead and let's see. Is there anything else we have to do? Well, I think we are okay for now. We can do the margins. We can do this. All of that is great. Let's see how it looks on 100%. It looks quite good. Perfect. So now what I want to do is I want to go back inside of my nav bar and I actually want to develop this toolbar here. Well, at least I want to add all the menu bars that it is going to have. Great. So now let's add a new menu bar here. All right. So we have this menu bar. So this is a menu bar menu. Okay. So let's end it here and let's add a new menu bar menu with a menu bar trigger here. And this one will have the text edit. And let's give it a class name. Text small font normal py.5px will be 7 pixels. Rounded small hover background muted and height of auto. And I should also copy this entire class name and put it in our menu bar trigger above for file like this. So now there we go. You can see how similar they look perfect. And let me just expand a bit. There we go. Yes, I noticed something was off initially, but now it looks fine. Great. So we have file. We have edit. Great. Let's copy this menu bar menu. And now let's go ahead and let's add the next one, which will be insert like this. Let's go ahead and let's copy it again. After insert, we're going to have format. And that will be the last of our menus here. Great. So we can now go one by one and we can actually develop them. So let's go back inside of the file menu bar menu. And we're going to slightly modify this. So this file and then save will actually open up a whole new sub menu. So in order to do that, what we have to do is after the file here, we have menu bar content. And first of all, let's give it a class name of print hidden because you're going to see why in a second I will come back to this. But we need this because the print option will be one of the options here. So this menu will stay open while we trigger the print media query. So that's why we have to explicitly add this print hidden, even though this entire thing is technically hidden. Looks like it somehow escapes, I guess, because of portaling, right? The hierarchy becomes different when this is opened. OK, so now after menu bar trigger file, we have the menu bar content. And then instead of having menu bar item here, we should have a menu bar sub like this. And then instead of here, we should have a menu bar sub trigger. So this is a trigger for sub, not for the actual any other menu. And then we're going to have menu bar sub content like this. And then we can have menu bar item finally. So let's add a file JSON icon from Lucid React. And let's give it a class name, size 4 and MR of 2. And let's write JSON. So now if I click on file, there we go. JSON pops up. So we will enable all of those things. So let's now copy and paste all of these menu bar items instead of the menu bar subcontent. So after JSON, we're going to have HTML. So this can be a globe icon from Lucid React. After HTML, we're going to have PDF. And this can be bs file PDF. And let's see, where can we import this from? from React icons BS. So let me just move this to the top so you can see it here. There we go. BS file PDF like that. And the last one will be text. And we can just use file text icon from Lucid React from that. So just make sure you have all of these imports from Lucid React. So now you can see that when I click on file save, I have all of these options to export my content of the editor into all of these different formats. so now let's add a couple of more things in this initial file menu here so after the menu bar sub ends we're going to add a pure menu bar item here and we're just going to say new document and we're going to give this a file plus icon from lucid react and the class name will be the same as it was in all of these other ones like this so now there we go we have save and we have a new document outside of the sub menu, right? Because we closed the menu bar sub here. And then what we're going to do is we're going to add a menu bar separator here. And we can now add a menu bar item again. This one will be to rename the document, which we cannot do yet, right? Because, well, this isn't really saved anywhere. So we're going to be using the file pen icon for that, for Music React as well. Let me just go ahead and copy this. There we go. Then we can go ahead and copy and paste this one more time. And this one will be removed. And we're going to be using the trash icon from Lucid React. So make sure you have the trash icon from Lucid React like this. And then what we can do still inside of the menu bar content here is we can add a menu bar separator again, which is a self-closing tag. And then another menu bar item. and this one will have a printer icon from lucid react and let me just immediately copy the class name here and we will simply say print like this so now we have save a new document rename remove and we also have print and what we can do here is we can also add a menu bar shortcut like this and now i honestly have no idea how to even add this sign this is what i'm talking about This like command sign, I have no idea how to do that. But yeah, you can, I think you can Google, let's see, command sign Unicode. Yes, I guess this is the Unicode, for example. And you can just like copy it from here then, I guess. And then like paste it here. If you really want it, you can just leave it at, you know, P if you don't want it. Great. So now you can see the print to be here. And we can already enable the print, which is cool. So we can click on click here and do window.print like this. There we go. So I can click file. Oh yeah we have an issue The issue is that the navbar ought to be used client So let add use client here so we can actually do reactive things here So let me refresh this page. Click file, click print. And there we go. You can see how our dropdown menu was actually omitted from the print, right? So we cannot see the navbar. We cannot see the ruler. We can only see this default text things. And we can even modify this as well. But you can see that most of the things printed are actually just our content from this page right here. Perfect. And the shortcut will also work here, right? Because, well, it's the same thing. Perfect. So all of that is prepared here. So what we can do now is we can go ahead and do the same thing, but for the edit menu. So we can just prepare things semantically, and then we're going to add them in a moment. so we can go ahead and find where our menu bar menu for file ends and our menu bar menu for edit starts and after the menu bar trigger we can add a menu bar content here and we can add a menu bar item here and we can give it an undo to icon from lucid react and we can write undo like this so There we go. We have the undo here and we can just style a class name. Same as always right here. Make sure that you actually imported this icon. I'm just going to collapse all of these so you can see them nicely here. Like this. There we go. So you can now see all of the files, all of the imports which I have from Lucid React. And we can also add a menu bar shortcut here. and we can again you know copy that command uh icon from above right this one and paste it here but with a z like this there we go so we now have edit undo we can copy the entire menu bar item here and call this redo and we can also import a redo to icon from lucid react like this this will be command Y like this. There we go. So we're going to have undo and redo. Great. So now what we can do next is actually go immediately inside of the insert here. So what we're going to do here is add a menu bar content after the menu bar trigger here, and we're going to open a menu bar sub like this. And inside of here, we can add a menu bar sub trigger and it will simply be a table. So we're going to add the ability to insert table from here. So menu bar sub content now will give us a couple of menu bar items. And we're going to start by one by one table, and then we're slowly going to increase that. So two by two, for example. And you can of course do this completely, you know, dynamically self-generating. You don't have to hard code this, but you know, I'm just trying to demonstrate how to use a nav bar at the moment. But yeah, you can of course like create a matrix of all the combinations you want and just render them outside here. So now insert gives you all of these options for the table. So you will be able to insert a two by two table, three by three, four by four, or one by one table here. Great. So, of course, we have to enable this later. And now let's go to our last one, which is the menu bar trigger for format. Let's add another menu bar content here. Let's go ahead and just make sure it's closed. Now let's open a menu bar sub. Let's open a menu bar sub trigger. And the trigger will be our text icon from Lucid React with a class name of size 4 and MR of 2. and a text will be the actual text inside of this subbar trigger. Make sure you have imported the text icon. There we go, right here from Lucid Reacts. And now instead of format, you can see that we can format our text. Great. So let me just scroll all the way down here inside of our menu bar sub trigger. And we are now going to add a menu bar sub content. And then we can add the menu bar item inside. this one will have bold and we can also use a bold icon from lucid react with a class name size four and mr of two like this so now when i click on format text and we can then bold whatever will be selected at them at that moment so just some additional options the same as we have here it would be cool to have them in the format as well and we're going to practice how to do that as well so we can copy this a couple of times instead of the menu bar subcontent the next one will be italic like this and we can use the italic icon from lucid react the next one can be underline icon and underline like this and if you want to you can also add another one which will be the strike strike through icon from lucid react right so strike through icon from lucid react all of this so make sure you have the bold make sure you have the italic make sure you have the strike through icon and this will be strike through like this there we go so now when I click on format text you can see I have all of this and the cool thing we can also add here are some shortcuts so you can go ahead and do menu bar shortcut here and you can again use that command sign with an s like this and let me just copy this so for this one it will be a u for this one it will be an i and for this one it will be a b as in bold so there we go now all of these have their respective shortcuts here as well great And so now let me see what we can do next. Outside of the menu bar sub here, we are going to add one more menu bar item, but it will be outside of this sub menu, right? So it's not going to be related to the text formatting, but this will simply be a remove formatting icon from Lucid React with the same class name, size4MR of two, and clear formatting is going to be the text like this. So there we go. Now in format, you can either clear formatting or you can do any of these options. And let's see, can I kind of improve this? I'm not entirely sure. Maybe if I wrap this inside of a span and give this a class name of truncate. Can I improve it now? It looks like it's not improved. Max width 50 pixels maybe. Uh, yeah, but then we mess up this. Yeah, so I'm not sure. But, oh, I think I know a way that we can artificially improve this. After here, I can add an MBSP, which simulates like an empty line here, empty white space. So I can add like two of those. And there we go. Yeah, that improved it. Great. So we have the UI ready for our nav bar here. We've added a lot of things here. We even enabled one, which is the print one, which is pretty cool already. So what we're going to do in the next chapter is actually develop all of those things. And we can do them, all of them besides new document, rename and remove. Everything else we can do because it's editor related. And we already have all of those functionalities here in the toolbar. Great. Amazing, amazing job. Before we go on and implement everything inside of our new navbar, I've noticed an issue with our ruler here. If you notice, my right side marker is just after the tick number eight. But if I zoom so much that I get an overflow, what happens is that my tick has now moved closer to number six than previously, which it was here on eight, right? So we need to resolve that. We need to make these have fixed position. And we can do that by going inside of source app documents document ID directly inside of the ruler here. And then what we are going to do is the following. We're going to focus on the main div here and we're going to give it a width of 116 pixels like this. And we are also going to do MX auto here. And then instead of here, we can remove everything besides full width, full height, and relative in the lower container. And now let's take a look at the same thing. So I'm going to go ahead and move my blue tick on the right side directly on number eight. And what happens if I zoom in? As you can see, this time the overflow is working correctly and I can find it here at eight. Perfect. So we resolved that issue of ours. Now, what we have to do is we have to implement the entire navbar. So let's go ahead and do that. Let's go inside of our navbar right here. And let's go ahead and do an easy one that we can already do, which is the edit, undo, and redo. So the first thing we're going to need is we're going to have to somehow get the editor inside of the navbar. And we can do that by extracting the editor from our use editor store. From store, use editor, store. Let's check if we have marked this as use client. We have. Confirm yourself that you have it. And now we can go ahead and we can implement the undo and redo. So let's find the edit menu bar. There we go. And on the menu bar item, let's add on click here. editor, chain, focus, undo, run. There we go. And then we can copy this thing and paste it here and call a redo this time. So now I'm going to go ahead and write some things. And if I click edit, undo, there we go. And redo, both are working flawlessly. Now let's go ahead and let's implement the table insert. So in order to implement that, let's go ahead and create a reusable method here called const insert table. We're going to accept rows and columns. And let's give this a type. So both rows and columns will be a number. And then let's do editor? Question mark chain dot file.\nfocus, insert table, rows, columns, and with header row can be false. And let's do run like this. And now we have to use our insert table method. So let's go ahead and find our insert. You can recognize it by those. There we go. One by one, two by two. So on one by one, we're going to call the on click here and we're going to insert rows one and columns one. And you can already guess what we're going to do for all the other ones. So exactly as the numbers say here. So two by two like this. Then we're going to do three by three here. and then we're going to do, did I wrong button here? There we go. And then we're going to do four by four. There we go. So now what we can do is we can go inside of our editor and we can remove the initial content, right? We no longer need this to be a table. We can remove it entirely like this. So now when you refresh, you should have an empty document here and you can use the insert to add a nice table like this. Let's try a smaller one. There we go. Our tables are working. Perfect. So that is resolved. Now let's go ahead and let's resolve the format ones because we already have all of this method. So it's going to be easier than our save function. So let's go back inside of the nav bar here. And let's go inside of the format. We are already here. Great. Let's find the menu bar item for bold. And let's click, let's right on click here. And let's do editor, chain, focus, toggle, bold, and run. Like that. And now we're going to copy this. In here, toggle italic. In here, we're going to toggle underline. And in the last one, we're going to toggle strike like this. There we go. So let's try it out. I will select this, click on format. Bold seems to be working. Italic seems to be working. Underline seems to be working. And strikethrough seems to be working as well. And you can see how all of them get selected in our toolbar here. Excellent. So now what we have to do is also the clear formatting one. So let's also resolve that. So we're simply going to call unset all marks here like that. So let me select this. And let's click format, clear formatting. And there we go. Everything was removed. Great. So now let's go ahead and let's implement our save methods, right? So file, save, we need to implement this now. So I'm going to go inside of my navbar. I'm going to go to the top here. And I'm going to implement a reusable on download method. So we're going to use this for each of our download ways. So first of all, let's add some props here. So blob will be a type of blob. And file name will be a string. And then let's go ahead and create a URL from the blog. And now let's create an anchor element, which we are going to programmatically click on. So create element A. A.href will be our URL. A.download will be our file name. And then we are going to click on that anchor link. Now let's develop on save json for example. If we have no editor we can already break this method. Otherwise let's get the content using editor.getjson like this. Then let's define the blob by using new blob json stringify like that and passing the content. Go ahead and add some options to be type application slash json. and let's call on download here, pass in the blob in the first argument and go ahead and do data.title. Oh, we currently don't have this yet. So this is what we're going to do. Document.json. And I'm going to add a to-do use document name. We currently don't have this because the document is not saved in the database, but later we'll be able to do that. Great. So we now have on save JSON. So now let's go ahead and let's add on save HTML. So it's going to be similar. So we get the content this time in get HTML format like this. We create the blob, but the blob this time will be the content directly here. And the type will be text HTML like this. And we will save this as a dot HTML. and let's go ahead and create another one which will be on save text like this so this one will use get text the blob will be content and the type will be text slash plain and we are simply going to use dot pxt here there we go so we now have all three methods and we can now finally attach them so let's go ahead inside of our save sub menu choose the first json menu bar item on click is going to call the on save JSON like this. And I'm going to copy this now and do the same thing here on save HTML. The PDF can do a trick here. So we can just call print again, window.print because this will trigger an HTML, a PDF download. And let's do an on click here on save text. There we go. So now all of this should be working. So if I click Save JSON, there we go. I now have a JSON document here. If I click on Save HTML, there we go. Let me just try and open this. So you can see document.json, document.html. And if I try PDF, there we go. I have print and I can save as PDF here. And let me also try text. and this should just save the text. There we go. JSON, HTML, and text. All of them are working. You can, of course, play around and there we go. So this is the JSON. You can see this is tip-tap content inside. That's one example. What else do we have? We have HTML. There we go. You can see that it renders an HTML. Looks like the tables... So the reason this is not displaying the tables is because this is just an HTML file, right? So you would need to add the styles for it, but you can see that it uses the tables, right? So it does know that this is a table, but this empty index HTML does not have the styles for the table, right? So that's why the table is not displayed here, but the table is definitely in the code, which we just saved. Great, so that brings us to the end of our navbar right here. So what we are finally ready for is to start implementing some additional things to this editor, like live collaboration, and things like saving the content in the database, creating some templates, and also authentication. Great, great job. So what we're going to be building next is back on our homepage. So if you don't remember, all of this work we've been doing so far is located on localhost 3000, slash documents, slash one, two, three, right? So right in this position. So what we're going to do now is we're going to go back instead of the app page.tsx. We're going to go back here on the root page. So you can click on that logo because this is where we're going to continue our development. First thing I want to do actually is change the overall font of my application. So I want to go inside of layout.tsx right here. And I'm going to import inter from next Google. My apologies. Next font Google. And I'm going to remove local font. And I'm going to remove both of these gaste imports here. And inside of my class name here, what I'm going to do is remove everything. And I will simply prepare my inter font here. so const enter will be new enter and i will simply add subsets to be latin like this so let's see without new my apologies so just enter and we're just going to pass enter.class name here and now when you refresh you should see a completely new font and you can even visit your individual document ID page, and it should look slightly different, slightly better, in my opinion, more similar to that Google style of font and applications. Great. So let's continue back on this homepage here. So what I want to do now is the following. I want to change my app structure a bit. So I'm now going to create some components which are specifically going to be used for the homepage, right? So similarly, like I've created some components inside of the document ID folder, I now want to create some components for this page file right here. And I can technically just create them here in the app folder. But remember what I can do, I can create a route group, which will not be a part of the URL. So I'm going to go ahead and create a new folder. In parentheses, I'm going to write home like this. And I will drag and drop this page and put it inside like this. And if I'm asked to update the imports, I'm going to press yes. And then you can see that I have one unsaved file in my dot next folder, which is basically just cache. So it doesn't really matter. But you can just enter that file and save it and close it. And just also collapse the dot next folder. So you don't accidentally develop inside of that folder. So now your page dot TSX, which was usually in the app folder, is inside of the home folder. But the functionality is still exactly the same. So my LogoClose 3000 is still displaying this page right here. Great. So now let's go ahead and let's develop a custom navbar for the homepage. So I'm going to go ahead and create a navbar.tsx like this. And I'm going to import link from next link. I going to import image from next image And I going to go ahead and do export const navbar here And I will return a nav element like this And I will write navbar inside. Before we go any further, I want to go back to my page. And I actually want to render this navbar here. So I'm going to go ahead and import that. So import navbar from dot slash navbar. I'm going to modify this main div here a bit. So minimum height of screen, flex and flex column like this. And then inside of here, I'm going to add a div encapsulating my new navbar. And then what I'm going to do is I'm going to give this a class name of fixed top 0, left 0, right 0, Z index of 10, height of 16, and the background color of white. And I'm going to wrap the rest of my content here, which is basically this text and link. And I'm going to give it a class name of margin top of 16. So we offset by the height of this fixed navbar here. So there we go. Now we have the text navbar here. And then we still have this little shortcut for us to go back to a document. So now let's go ahead and actually develop navbar here. So I'm going to go back inside of my navbar. And now we're going to do similar to what we did in our document ID navbar, which is add an image. But let's first style this. So I'm going to give this nav element a class name of flex, items center, justify between, pull height, and pull width. Like that. Then I'm going to go ahead and add a div element with a class name, flex, gap three, items center, shrink zero, and pr of six. And then I'm going to add a link, which I've already imported. I'm going to give it an href of root page. And I'm going to add an image component. The source here will be my logo.svg, which we have already previously used. Alt will be logo, width will be 36, and height will be 36 as well. And then we can add an H3 element here, docs, like this. And let's give this a class name of text Excel. There we go. So now we have a nice little element in our app here. Great. So what I want to do now is I want to quickly go back to my page.dsx. And I think that I have forgotten to add a padding of four here like that in this navbar. There we go. So I want the docs to be just a little bit separated here. Great. So that is my navbar. And now inside of that navbar, I want to develop a search input component, right? So after my navbar, I'm going to have a search input component. So let's go ahead and do that. I'm going to go inside of home and I'm going to create a new file, search-input.dsx, like this. I will export const search input here. Button return div. Like this. and I will just write search input like this. Then I'm going to go back inside of the nav bar and I'm going to render it here. Search input from dot slash search input, just like that. And now, as you can see, we have a little bit of an issue because my logo is here, but my search input is all the way in this corner. I want my search input to be here in the middle. So what I'm going to do is I'm just going to add an empty div here at the end because this will later basically be my avatar to log in, log out, and change organization. So for now, this is just an empty div. And there we go. You can see that now this is centered inside because of our justified between. So let's go inside of the search input and let's continue developing from here. I'm going to give this div encapsulating my future search input a flex one, flex items center, and justify center class name here. Then I'm going to open up a form element. I'm going to give the form element a class name of relative, maximum width of 720 pixels, and a full width. And then I'm going to add my components UI inputs right here. So there we go. You should now be seeing an input here. Great. Now let's go ahead and let's give this a placeholder of search. like that. You should now see the text search. And now let's add a class name here. So I'm going to change MD text base. So we overwrite the size of my text inside of here like that. Then I'm going to change the color of my placeholder, which is currently kind of gray. So I'm going to use placeholder text neutral 800. There we go. So just a bit darker. Then I'm going to go ahead and give this a px of 14 on both sides so i want a lot of empty space here and i want some empty space here because we're going to have a nice search button icon here and we're going to have an x icon here to clear our current search all right now let's add a full width here now let's add a border none then let's go ahead and modify the drop shadow which happens once we focus on this so focus dash visible. And now go ahead and add shadow dash and open square brackets like this. And now we're going to type inside of here, right? So right now, nothing much has really changed, but you can see we have some odd shadow here. And now we're going to improve that shadow. So go ahead and write the following zero underscore one pixel underscore one pixel underscore zero, and then another underscore RGBA and open parenthesis and inside write 65, 69, 73.3 like that. Then add a 0 underscore one pixel underscore three pixels underscore one pixel RGBA 65, 69, 73 and 0.15 like this. and I just realized a small mistake here. So in my first RGBA here, it goes 75, 69, 73, and then a comma like this, and then 0.3 like that. Great. So now that we have this, we are finished with the shadow. Let's add a background color of F0, F4, F8 like this. let's add a rounded full class and let's add a few final ones a fixed height of 48 pixels a focus visible ring zero and focus background color of white like this so this is the entire class name and now you can see that we have a much smoother and nicer looking search here and the effect is almost identical to Google's, right? So that's why I really wanted us to do this specific shadow here. So again, just make sure you don't use 73.3, but instead add a comma after 73 like that. Since I seem to be having a lot of this very long lines of classes, I'm going to make it easier for you. and I'm going to turn on wrap text. So word wrap, I'm just going to turn it on. And then you can see that you can always see my entire class name, right? So if you want, you can pause the screen. I hope this will be easier for you this way. Okay, so now that we have this, let's go ahead and let's add some icons, right? So after our input, I'm going to add a button from components UI button. So let me just fix this typo here. So let's add a button from components UI button. Make sure you have both of these imports here. And inside, we're going to add a search input. My apologies, a search icon. We have to import that from Lucid React. So I'm just going to move it here like that. And now let's go ahead and give this button a couple of attributes. So first of all, a type of this button will be submit. Variant will be ghost. Size will be icon. And then we're going to have a class name, absolute, left three, top one and a half, minus translate, Y, one and a half. And then we're going to override the SVG size. So, and underscore SVG inside of square brackets, colon, size five. And then rounded full like this. And then you can see how we have a nice button with the search icon inside. just like this. Great. So you can see how this already has a Google-like material feel. Great. So we have that. And now what I want to do before I implement the next button is just add some functionalities here. So let's go ahead and do that. I'm going to go ahead and add a value here and set value to use state from React. If this, and then I'm going to define an input ref to be use ref from react and default value will be null but let's give it also a proper type which is html input element like this let's define a handle change method here to be event react change event html input element like that and set value event target value like this and this will have to be a string like this. Well, we can just do it easier. We can just put an empty string in the beginning. There we go. So now that we have this, we can attach those things to our input. So let's go inside of our input here and let's pass in the value to be the value. Whoops. Let's pass in the on change, be on change. And let's pass in the ref to be input ref. And this is handle change, not on change right There we go So now our input should be controlled but we have an issue here That is because we did not mark the search input as a client component So we have to mark it as a client component. And there we go. No more issues now. So now we have a controlled input. Great. So now we can go ahead and finalize this part. So I'm going to do this. If we have value, meaning we have typed something. I'm just going to copy this entire button and add it here, but I will modify it slightly. So the type, this is important, the type needs to be button. Don't forget to do this because if you put a button inside of form element, it will automatically be considered a submit button unless you explicitly give it a type of button. And since this button will be used to clear our input, You will clear your input on every enter press. So just be mindful and put the type button here. And the icon is going to be X icon from Lucid React. So just ensure you've added that. Great. And now we're just going to modify this to be absolute and right three. So now you can see that when you have a text, the X button appears. When you delete it, it does not appear like that. Great. So let's go ahead now and let's implement the handleClear method. So const handleClear method here will be an arrow function, which will set value to an empty string, and it will call inputRef.current and call the blur. And just use the question mark here like this. So the handleClear method now will be called here on this X item. So if you write something and click here, there we go. It will be deleted like that. Great. So we are almost done. But what we are missing is the submit method. So what will actually happen once we submit this form? So we're going to handle that now. So for this, I will be using the NUCS package, which will allow us some really, really cool and easy manipulation over the search params in Next.js. So let's go ahead and install that package. So at the moment I'm writing, the current Nux version is 2.2.3. And it's actually quite important which version you use because this package had to undergo a lot of changes to support Next 15, right? So make sure that you're not using an outdated version, right? So when you install this, which we're going to do now, make sure that you use the proper version like this, right? Perhaps you have cached an old version of Nux and I'm pretty sure you're going to get some errors if you do that. So use a version at least 2.0. Again, in order to make this tutorial as easy as possible, just use the exact same versions that I am. And I already know that I'm going to have to put legacy here, that's here. And there we go. So we now have Nux. so let's go back inside of here and what we have to do now is we have to do one thing which previously we didn't have to do with nux which is we have to go inside of our layout.tsx and inside of here we have to import the nux adapter like this from nux slash adapters slash next slash app like this. So now we have the Nux adapter. And I'm going to go ahead inside of my body here and I will simply add the Nux adapter just like that. Now let's go ahead and let's create our hook, which is going to control the search params using Nux. So instead of my source folder, do I have hooks? I already have hooks here. I assume this is from ShatCN, right? Because we did not create any custom hooks here, I believe. We can, I think, confirm that if I call use mobile, there we go. The sidebar uses use mobile. Great, yes. So this was definitely added here with our ShatCN add all components functionalities. inside of this hooks folder now, create a new file, use search param.ts, like this. And now we're going to import parse as string, and use query state from Nux. And we can export function, use search param, make the key a type of string, and simply return use query state. Pass in the key as the first argument, and then we're going to add some options. Parse as string in the second argument. Make the default search query be an empty string, and with options, clear on default, true. I'm going to explain what these options mean in a moment. Great. So we now have this useful use search param here. And what we can do now is we can go inside of our app folder, home, search input here, and we can import that hook. So use search param from hooks, use search param. We can then add it here. So search, set search, use search param. And we have to add the key, which is basically what's going to be written in the URL. So this is how this works, right? Let me show you. Uh, basically the use query state is the same thing as X set X use state from react, right? But with Nux, you can do URL X set URL X from use query state like this, right? So this is how you can imagine this works. Uh, what's cool about use query state is that it will automatically synchronize whatever values we work with programmatically, which we are going to work exactly the same way as we would with use state, but it will synchronize them to the URL search params, which means that you will be able to find URL X one, two, three inside of your URL at the top, right? So that's what we're going to do now. And in here, I just created a reusable use search param, because I don't want to type out this options every single time. So this basically means that by default, we want every search param to start with an empty string. And clear on default means that If we call the use query state returns, again, let me just go back. If we call set URL X, for example, back to undefined somewhere, we don't want the URL X to be undefined. We don't want it to be null. We don't want it to be false. In case we get any falsy, we're going to clear this. So it's just going to remove that part from the URL. That's what the second part with options does. So now let's go ahead and add that. And we just have to find proper ways of using it. So now let's implement our handle submit because we have not done it yet. So const handle submit here. We'll take an event and it's going to be a react.form event. HTML form element like this. Let's prevent default. Let's set the search to be value. So set search coming from our new use search param. So we are adding it to the URL. And let's do inputref.current?blur. So we unfocus from that. And now we can assign handle submit to our on submit on the form. Just like that. There we go. So now my URL currently has nothing inside of, has no search params, right? But if I write hello world here, you can see that I have a question mark equals hello world. So we forgot to do one thing, which is pass a proper search barum inside of here, right? So what is this stored as in the URL? Well, whatever you want to do, for example, search, right? Now I'm going to go ahead and go back to my localhost 3000 here. And if I write test, you can see that my localhost 3000 now has search equals test. If I change it to test123, you can see that it now changes to test123. So I recommend that you name your key in the URL search because I'm going to be using search throughout the tutorial. So to avoid errors, it would be quite useful for you to also call it search like that. There we go. So and since this will technically be the only URL search program in our app, what we can also do is just hard code the key, right? You can also just write search here and ignore the key like that and then just remove this part like this. So this way you don't have to worry when you use this in multiple places if you accidentally added a typo to the search. So I'm just going to demonstrate this again. I will just remove this part of the URL. Hello world. And there we go again. Search hello world like that. What we have to do to wrap this up is also call the set search here in the handle clear like this. So back to nothing like that. And also let's use this search value as the initial value of our use state here. So they are kind of synchronized. So now when I click on clear here, you can see how it disappears from my URL. Let me show you what would happen if we didn't have this with options. So I'm going to go ahead and refresh, write test, and then I will delete this. Oh, well, it looks like it works even without that. But I'm pretty sure that this will clear it on other false methods, right? Because without this option, it is possible for your queries to be like search and just this, right? It's technically empty, but why is it in the URL? Whereas if you put this onto true, this cannot happen, right? This will always be omitted. Great. So that wraps up our search input. We no longer have to do anything else here. And also you can use this to submit as well. so what we going to do next is later on we going to use the search query to actually query through our documents and find something with a name similar to that But what we going to do next is we going to implement a template gallery right here below our navbar Great, great job. now that we have our search input component finished let's go ahead and continue developing our home page by adding a template gallery here to allow users to choose different types of templates which will later transform into new documents which is our page here so go on to the homepage here. And let's go ahead and let's go back inside of our homepage.tsx, just like that. So what I'm going to do is in this MT16, this time, I'm just going to remove everything inside. So nothing besides my search will be here. And then I'm going to add a template gallery component like that. Let's go ahead inside of the home route group and let's create a template-gallery.txt. dot t s x just like that now inside of here let's just see so we have to mark this as use client let's do that first and then let's go ahead and export const templates gallery just like this i will just return a div templates gallery so now i'm gonna go back instead of my page and I will import the template gallery. And I can see that I'm not exactly consistent with the name. So this is templates and this is template. So let me just rename the file to be templates like this. And I will update the imports in the page here. So templates gallery and templates gallery. There we go. And now we have a text templates gallery here. Now let's go back and continue developing inside of the actual templates gallery. So I'm going to give this main div here a class name of background, F1, F3, and F4, like that. Then inside of here, I'm going to open a new div, which should be, of course, lowercase. And I'm going to give this a class name on maximum with screen extra large, MX auto, PX of 16, PY of 6, flex, flex call, and gap Y of 4. And then inside of here, I'm going to add an H3 element, start a new document. There we go. So now we have a text which says start a new document. Let's go ahead and give it some slight styling. So it's going to have a text base and font medium. There we go. And I actually think that we don't have to put text base explicitly because that is the default text size. Great. So what we need to do now is we have to import our carousal component. And we already have the carousal component from components UI carousal, which we added from ShatCNUI. So it should be located inside of your source components UI carousal right here. So now that we have the carousal, let's go ahead and let's actually create it. So we're going to go just below the H3 element and I'm going to add my carousal here. Then inside, I'm going to add a carousal content and I'm going to give the carousal content a class name of minus ML4. and then inside of here what we have to do is we have to iterate over our templates right so currently we don't have any templates so let's just go ahead and do this const templates let's make this an empty array and let's simply add a single object inside i'm gonna give this an id of blank i'm gonna give this a label of blank document like this and i'm gonna give it an image URL. And let's see, I'm going to use slash logo.svg because that's an image that I have inside of my public folder. And you should have it as well because we've used it many times already. So we are going to use this as kind of the only template that we have at the moment. So let's go over our templates.map. We are going to get the individual template here. and then we're going to render the carousal item. So the carousal item will have the following attributes, the key, which will be the template ID, and the class name, which will be the following. Basis, one and a half. On small devices, basis, one third. On medium devices, basis, one fourth. on large devices basis 1 fifth on extra large devices we're going to have a basis of 1 sixth and on 2x 2xl devices we're going to do the following basis now unfortunately there is no 1 seventh that does not exist so what we have to do is we have to open square brackets like this and add 14.285714. Of course, you can probably use a smaller amount of decimals, but this was, you know, chatGPT generated class name for one sixth. But you can see they also use this amount of decimals in these ones. So I guess it is appropriate to use this one here. And let's also add a PL of four. So let me zoom out a bit so you can see this is, of course, just one command like that. There we go. So now inside of the carousal item, what we're going to do here is we're going to add a class name, cn, which we are going to import from lib utils like this. And we're going to have some dynamic classes here. So in the first one, we're going to have an aspect ratio of three quarters like this, flex, flex column, gap Y 2.5. And then we're going to go ahead and for now, this is what we're going to do. We're going to have a constant is creating set to false, which will later be dynamic. So I'm going to do is creating. And if it is, we're going to set the pointer events to none and opacity to 50 like this. And finally, inside of this div, we are going to render a native HTML button. First of all, this button will be disabled if we're currently creating a document. And then on click here, what we're going to do is just add an empty arrow function like this. And now let's go ahead and let's add some style here. So background image will be the following open backpicks, URL, template.image URL, like this. And while we are here, we can also add background size to be cover. We can add background position to be center. And we can add background repeat to be no repeat, like this. And then let's add some Tailwind classes. So size full, which is basically a shorthand for height full and width full. On hover, we're going to have border blue 500, rounded small, border, border, my apologies. on hover background blue 50 transition flex flex column items center justify center gap y4 and background color of white like that and then finally inside of here we're going to have a paragraph which renders the template dot label and this will have a class name text small font medium and shrunk eight. So now there we go. You can see that we actually have our document right here, but we do have to modify one thing. This paragraph here should not be inside of the button. It should be outside of the button below it like this. So now we just have to modify a few things here, it seems. First of all, this button can be a self-closing element. So we can close it here like this. There we go. That's the first thing. The second thing is, I'm not sure if this should perhaps be centered. Maybe it's better if this is centered, but we're going to work on that later. So what do I want you to do now? Well, I want you to actually find some proper images for our templates. And I have prepared for you this public guest, which I will share in the description of the video. Or you can, of course, visit the source code and look at my public folder. So what I want you to do is to download the blank document.svg. So I'm going to go ahead and save all of this one by one. Blank document, business letter, cover letter, letter, project proposal, resume. I butchered that. Resume, I guess that's how you pronounce it. And software proposal, right? So basically all of these templates which I have prepared for you, right? So these are images for our templates which we are now going to use. Okay, so once you have downloaded those, you have to copy them and paste them in the public folder. Like this. There we go. So we now have project proposal, resume, software proposal. We also have cover letter, business letter, and blank document here, and also letter.svg. So now, once you have all of those in your public folder, which are available publicly here, we can go back inside of source app home templates gallery here. And what I actually want to do now is I want to create a folder called constants inside of my source folder. And I'm going to move the templates here. So templates dot TS like this. So let's copy what we already have. There we go. Like that.\nAnd now let's just add an export const of the templates and let's collapse this here. So this first one will have a blank document.svg, which is basically what I have added here, blank document, this one, right? This first one. And if I refresh and also one thing we have to change is to actually use these templates now. So let me remove this and let me import the templates from constants templates like this. There we go. We now have a blank document here. So now instead of the templates here, I'm just going to collapse these things like this because later we will have one more field here, which is the initial content, which will actually be preloaded in our page. But we're going to handle that later. So after this, we're going to have a software proposal. So let's add a software development proposal here. And let's add software proposal SVG. So you can also copy the name from here if you want to. Great. So we have the software proposal now. There we go. That's the second one. Let's copy and paste this. After that, we're going to have a project proposal. So project proposal. And I will change this to project proposal. There we go. We now have the project proposal. Let's go ahead and add a fourth one, which is going to be the business letter. So let's rename the label to business letter. Let's change the image URL. there we go, we now have that as well, now let's add a resume, so ID will be resume, same as the label, and same as the image URL, there we go, and just a couple of more left here, so we're now going to have a cover letter, so cover letter, same as the image there we go we have the cover letter and lastly let's have a normal letter like this so this will just be a letter and it will use letter.svg there we go we have all of them here. If one of yours is missing, double check that you didn't misspell something, double check that you have no typos in your text here, confirm that you have all of those inside of your public folder. And as always, you can visit the link in the description or my source code to copy these documents for free. Now, let's go ahead and do something else. So if I zoom in, At the moment, you can see that only four of them are loading and I can now kind of drag them in a carousal way to load them. But what's missing are some kind of arrows which will allow me to go back and forth. And we actually have that. We just haven't implemented it. So you can see that we did include the carousal next and the carousal previous imports, but we did not use them. So after we iterate still inside of the, sorry, outside of the carousal content, but inside of the carousal, add the carousal previous, add the carousal next like this. And I think that should actually be it. There we go. we now have these arrows, which by default on your desktop device will be disabled. But if you zoom in, you can see that now we can use them to go backwards and forwards. Great. So we now support smaller devices as well. Perfect. So what we ought to do next is finally start connecting to our database. And once we click on one of these documents, I mean templates, we should actually generate a new document, which will redirect us to documents and then the ID of that newly created document. And depending if we selected a blank document or a template, we might pre-fill some content here. And then we can also add collaboration between users. Amazing, amazing job. Now that we've wrapped up our template gallery, we can finally go ahead and create our database and actually enable this gallery to create a new document in our database. By doing that, we will be able to store the content we are writing in each and every document. And we are also step closer to live collaboration in those documents. So let's go ahead and let's add a real-time database to our project called Convex. Head to convex.dev. You can also use the link in the description and go ahead and log in or create an account. Once you have created your account, you will be redirected to Convex Dashboard. As you can see, I already have one project. This is from my initial development of this very project that we are building. So what I'm going to do is I'm going to click create a new project and I'm going to call this docs tutorial like this and I will click create. Once we have created that, we can leave it as it is. Later, we can play around with this. I recommend that you actually keep this dashboard open because we will come back to this. But for now, you can just head back to convex.dev. And this time, let's go inside of the documentation. Inside of the documentation, let's select our next JS here. So we already have created our next app. So no need for that. But what we do need is to install convex. So let's go ahead and do that. And we will be using a specific version as well. So I'm going to expand this. And I will write npm install convex at 1.17.3, like this. And I forgot to run legacy depths. So let's just run this again with legacy peer depths like this. So we are installing convex at 1.17.3, which at the moment, I believe is the latest version. Let's click on the versions here. There we go. 1.17.3. I just had to refresh because it was displaying 1.17.2. So yes, this is the version I will be using in this project. Great. So we now have convex and now we have to run npx convex dev inside of our app. So yes, we will be have to running both our front end using npm run dev and our convex separately using npx convex dev. So make sure both of those are in the same project here. So what would you like to configure? In our case, we can choose an existing project. So let me just use my arrow keys to go down and select choose an existing project. If you're running this command for the first time, you will most likely first have to log in to Convex, right? You will get a redirect back to the Convex dashboard. You will have to log in with your GitHub and you will basically have to log in an account which has these projects right here, right? Let me just go into docs tutorial here. Okay. So, and once you submit your account, once you are successfully connected with your terminal and your convex account, you can go ahead and select your project. So in my case, that is docs tutorial like this. And there we go. So now you're going to notice that inside of our dot environment dot local, we have convex deployment and we have next public convex URL. So all of that here is set for us. On top of that, we also have the convex folder where we are going to generate our schema, our API, and all the other things we are going to use. So let's go ahead and try and do something. So this is what I'm going to do. I'm going to go ahead and immediately learn how to build a schema here. But just before we can do that, let's do this. creating a convex client provider because that is kind of without this, we can't even run convex inside of our project. So let's do that first. I'm going to go inside of source and I'm going to go instead of components and I will create a convex-client-provider.tsx like this. So I'm not doing that inside of the UI folder. I'm doing that directly inside of components here. I will mark this as use client right here and then i can go ahead and just copy everything from here like this so we are importing the convex provider the convex react client and react node from react right here there we go so all of that is working and now we can go inside of our app folder inside of layout here and we can go inside of the nox adapter and add the convex client provider from components Convex client provider, make sure you don't accidentally do some other import here. So let's just wrap our entire app into that. There we go. So I'm going to go ahead and just revisit my app just to confirm that everything is still working. Nothing is broken. I can search. I can remove search. Everything seems to be completely fine still. Great. So we just did this step. And now we're going to go ahead and do this. But instead of us populating the database using a JSON file, I want us to immediately learn how to actually create a schema and how to create an API endpoint using Convex. And then I'm going to show you some cool things about how Convex works. So let's go ahead and learn how to do that. I'm going to go inside of the database and I'm going to select a schemas here. so let's import these two let's go inside of our convex folder and let's create a new file schema.ts and we can close this generated folder here let's add define schema and define table from convex server and let's import v from convex values here and then let's go ahead and we can just copy this here and remove everything inside so we are defining the schema right now and for example So let define well let actually immediately do documents like this So documents will be defined table And we going to give each document a title, which will be a string like this. And the moment we save this, you can see that if we accidentally create a wrong schema, we are getting errors, right? For example, in this case, I was missing a default export, for example, this, right? So I just did const test, something like that. When I save, you can see that I'm getting errors. So the npx convex dev is actively looking at our changes. So we have to export default, the define schema here, and then it will automatically synchronize that with the cloud. So now if I go inside of my docs tutorial project, right? You can see that immediately I have the documents here. And the cool thing is that I can actually click show schema and I can see the very thing that I have just written here, right? So I can debug much easier in this way. So what I'm going to do is inside of these documents, I'm going to add a document and I'm going to give it the title of test like this. And we can actually copy these a couple of times. So test one, test two like this. And let's click save. There we go. We now have three documents inside. And now let's go ahead and learn how to fetch those documents. So instead of convex, I'm going to create a new file documents.ds like this. And let's go inside of our documentation here, reading data like this. And we're going to import the query from dot slash generated server like this. And we can export const list. task. We can copy this entire thing, but we're going to slightly modify it. So I'm going to call this get documents, or we can just call it get like this. And we're going to query documents. You can see how type safety already works here and we can immediately return them. We don't even have to do anything besides this. And there we go. We just have a working get query here. And you can see that it will return the ID creation time and our custom field title, which we have defined right here. If I were to add something else like room ID and make that a string, you can see that then this get changes to include that room ID. But let's remove that for now. We're not going to need it. And I'm just, let me just double check if my data is still okay, because I just added that room ID. Yes, everything is okay. And the cool thing about Convex is that it's a completely real-time database, right? You don't have to do absolutely anything to make it more real-time than it is. So every single user connected to your application will see all changes in real-time. Every time you rename a document, every time you create a new document, all of those things will happen in real-time to all users on your platform. So that's a really, really cool thing about Convex. On top of that, there's a billion other things of why I absolutely love working with convex. But for now, let's learn how to actually call this API endpoint, which we have just created. So I'm going to go inside of source, I'm going to go inside of app folder, inside of my home here and inside of page, I'm going to go ahead and mark my home as use client like this. And then inside of here, I'm going to call documents to be use query from convex react like this and i will call api which i can import from convex folder so basically we have to go all the way outside of the source folder inside of convex generated and get the api right so you can use autocomplete for that right dot slash let's go ahead and try and do it together so you can navigate like this you are now here now you're outside and now you can get the convex generated API like this. And now we're going to target API documents get like this. There we go. And now below the template gallery, I'm just going to do documents.map. I will get the individual document here. And I will simply add a span document.title. And make sure to put a question mark here for documents map iteration. I'm going to tell you why in a second. So let me go ahead and try this and there we go we have test test one and test two great and what we can also do is if documents is undefined in that case we can return a special loading right so if documents are undefined that means they are loading so there we go you can see how now i have a loading and then we load we have some errors here that's because i did not assign a key here So that will be document underscore ID. And there we go. Test, test one and test two. Great. So what I want to do now before we end this chapter is create a proper schema for my documents. Because right now I have just created this so that you can see how convex works. So now let's go ahead and create a proper schema. So the title can actually stay the same. We will be needing that. And then let's add initial content, which will be optional string. So that's if we are using a template, right? If we are using a template, we're going to have some initial content inside. Basically, if we are selecting any of these, they will have initial content. But if we are selecting a blank document, then there will be no initial content for the user. Let's add an owner ID, which is basically the person that created this. And we're going to add authentication in a moment. But for now, this will just be a required string. We're then going to have a room ID, which is later going to be used for collaboration, which is also going to be optional because we will not be needing it while we are creating a new document, only once the user finally joins the room. And then let's go ahead and add an organization ID, which is also optional. So we are going to be using the organization and room ID to enable collaboration with permissions. So we are going to be very strict with who can visit our room, only people within our organization who we will be able to invite. And those invites will actually be sent to users emails, which is very, very cool. You will also be able to modify users permissions like admin member, all of those things. So it's going to be very, very useful. And now let's go ahead and let's add some indexes or indices. I'm not sure what's the multiple of index. And you can learn more about them here. So basically, index is an optimized way to fetch specific collections, right? So we're going to create the first index here, index by Ovner ID. So basically, we need a optimized way to fetch all of the documents that we own, right? Without using filters, we are using an index on our owner ID. Then let's add an index by organization ID for the exact same use case, but if we are searching within a specific organization. And then we're going to add a bit of a different index, which is called a search index. And we're going to use that to do this search, right? If we search, we will search through all the documents with the name test. and we can use an index for that. More specifically, inside of AI and search, we have full text search right here. And then we can define search index here and define by what field do we want to search. And we can also go even granular and search within a specific organization ID or specific owner ID. So let's go ahead and do the following. Let's add search index and we're going to call the index search title. and then we're going to say that the search field is the title field and filter fields are going to be owner ID and organization ID. So we don't look through our entire database, but only for this specific owner of that document or their organization ID. And there we go. We just defined our entire schema. And what's cool is that you can still debug this entire thing in your convex dashboard. Let me go ahead and try and refresh this. And you will see our new schema here. Did I save this? Oh, it looks like something is wrong here. Yes. So schema validation has failed. That's why this is not updating, because we have added a couple of documents here. And now we are adding a bunch of new required fields here. So this is failing. So one way to resolve this is to simply remove all existing documents inside of our convex database. And there we go. Now schema validation has been complete. And if I click show schema, you can see that it has been updated. Great. On top of that, you can also explore all of the functions you've write. You can see that in here we have documents get. And you can see the exact code here, which we have defined inside of convex documents. So we have the entire get method written here exactly. So we can see if something was wrong here. We can follow the execution time, the cache hit rate, the errors, the function calls. We can schedule runs if we want to. And there's so many other things we can do here. We can do file upload. We can do cron jobs. We can even do some AI methods here. Basically, one of the most useful dashboards I have ever seen when it comes to databases. Definitely amazing job by Convex. And of course, to end the chapter, amazing job by you. You have set up our database, you have set up our schema. And what we're going to do now is we're finally going to go ahead and enable these things to create a new document. And then we're going to list out those documents here in table. Great, great job. before we go on and learn how to create api endpoints which will be able to create new documents we first have to add authentication to our application so let's go ahead and learn how to do that by following the convex docs let's click on authentication and let's select clerk Let's go ahead and let's create a clerk account by following this link. So just go ahead and log in inside. Once you are logged in you will be redirected to clerk dashboard And once you are inside go ahead and click create application You can see that I already have a bunch of projects here So now let's go ahead and click create application. I'm going to call this docs like this. And you can go ahead and enable any kind of login you want. You can add login via Slack, via linear. You can go ahead and disable Google. You can enable Google. You can turn off email or turn it on. You can add username. You can add phone numbers. So all of those things work out of the box with clerk. So feel free to play around and build it yourself. I'm going to be using the normal email and Google here. I'm going to call my application docs and I will click create application. But there are a couple of more steps that we have to do. So let's go ahead and do the following while we are already here. we can already start doing this. So I'm going to do the following. npm install clerk clerk next JS. So I'm going to be following these three steps here. And I'm not going to do this one because we have to combine this with clerk. But these three steps are actually okay to follow. And here's an important distinction. If you follow get started in convex, they will tell you to use clerk clerk react right that's because this is an instruction for clerk with react app not specifically with next js but clerk has their own packages for next js so that's what we're going to be installing right now so after you've created your account and created your first project here you're going to go ahead and install clerk next js which at the moment is 6.5.1. So let's go ahead and do that. I'm going to do npm install at clerk next.js at 6.5.1. And I'm going to add dash dash legacy here, depths right here. And that should install my clerk. There we go. So what we're going to do next is go back to clerk here and let's add these values to our dot environment dot local so we need our next public clerk publishable key and we need the clerk secret key so let's go ahead and do that by going inside of dot environment dot local and let's add them here next public clerk publishable key and clerk secret key right here and after that let's go ahead and let's add a middleware.ts file so that we can access clerk in server components and ALF routes. Keep in mind that if you're using source directory, we have to create middleware in the root of our source directory, right? So usually you would just create it in the root of your project. But if you're using source directory, in that case, we have to create it inside of here. So let's create new middleware.ts like this. And let's just paste the content inside. If it's been some time since you've used clerk, one big distinction has happened. Previously, their middleware used to protect everything by default. That is no longer the case. So what this middleware does is it simply adds clerk to your Next.js environment. You can think of it like that. It will not protect any routes yet, right? So this is just adding it to the middleware. And you can see that if I go to my localhost 3000, I can completely access this page. There we go. Nothing has happened. Now let's go back to Convex and Clerk and let's see what else we have to do. So we created an application and we even installed some packages. And we can now do this. Create a JVT template. So let's go back inside of Clerk right here. and inside of our projects here, let's go inside of configure. I'm going to zoom in a bit and let's find JVT templates here. So my account, my project, and I'm looking at configure and let's click on new template and let's select convex right here. And you can pretty much save this entire thing. You don't even have to modify anything. So click save and then go back inside of the convex here because we will have to copy one thing from here. If you follow the instructions here, you can see they also give you a note. Do not rename the JVT token. It must be called convex, right? So if you've accidentally done that, you can create it again, but do not change the name here. And it looks like we have to copy the issuer URL. So let's go ahead and copy our issuer URL. It is right here. I will just click copy. There we go. What do we have to do next? We have to create out.config.ts inside of our convex folder. So let's go ahead and do that. I'm going to go inside of convex, and I will create a new file out.config.ts like this. And then let's export default. Let's return an object. And let's add providers here. An object again, domain. And I believe this is where we paste our issuer URL. And then we add our application ID to be convex here. So this convex name, I suppose, needs to be exactly the same as the name of the template. That's why they give you a hint to not rename the template. And you have to copy the issuer URL and put it into the main field right here. And let's go ahead and see if this is working. So I'm going to go ahead and clear this and do npx convex dev again. I suggest you do the same. So you can see from scratch, is everything okay? Convex function is ready. This means that everything is working nicely alongside clerk and convex. Great. But we're not done yet. So let's see. We already did this, but we did not install clerk react. We installed clerk next JS. And we also grabbed our publishable keys here inside of .environment.local. We have our clerk publishable key and we even have our clerk secret key. Now what we have to do is we have to modify this component, source components, convex client provider. What we have to do now is we have to combine this to work with clerk. So let's go ahead and do the following. I'm going to go ahead and I'm going to import convex provider with clerk from convex slash react dash clerk like this. And I will also, let's see, do I have to import anything else? I will also import from clerk, clerk react. I will import clerk provider and use out like this. I'm just a bit confused because I seem to be having this, this, but didn't we install the next JS version? I can't really look here because in my package JSON, I don't have clerk, clerk react, but in here it seems to be available. So I just want to make sure that I didn't accidentally install it somehow. But I think this will just work normally. So let's go ahead and try this. So what I'm going to do is I'm going to wrap my entire app here inside of this clerk provider. So clerk provider like this. and I'm going to give it the publishable key here to be process.environment. And then we can just copy the one from .environment.local here. So next public clerk publishable key like this. And we can put an exclamation point at the end. So we get rid of the TypeScript error here. And let me just check one thing. I'm really interested in this clerk package. can I so I installed clerk next js can I import the clerk provider from clerk next js instead I think I can yes so we can do this instead I'm not sure why the package react is available except if having next js automatically means we also have clerk react installed in peer dependencies That could be the reason why. But I think they share exports together. So I think this will work fine. So let's go ahead and let's switch this to clerk next.js because that's the package we have explicitly installed. So that's why I want to use it. So now instead of using the convex provider here, we're going to switch that to be convex provider with clerk like this. But we do have to modify something. We have to add use out here to be used out. And let me just collapse these attributes and let me collapse the actual children inside. And there we go. So we have added use out and client to convex provider with clerk. And I believe that we can now remove the convex provider itself. There we go. So right now, I believe that still nothing significant should change. You can see that I can still both fetch my data. I mean, they are empty because we removed the tasks, but you can see that it still loads for a second, meaning that I am allowed to fetch the data and I can still see this screen. So still, even though we have added authentication, nothing has really changed. And this is what we're going to work on now. So instead of convex react, we're going to import authenticated. we're going to import unauthenticated and outloading. So three of these, authenticated, unauthenticated, and outloading. And now what we can do is we can choose to only render the children if we are authenticated, for example. So simply encapsulate the children inside of authenticated And then for unauthenticated we can display a paragraph which says please log in And for outloading we can return a paragraph loading out Let's try it out. You can see that now it says, please log in. If I refresh for a moment, I have loading out and then it says, please log in. So how do we log in? Well, thanks to clerk very, very easily. All we have to do is from clerk next JS, import sign in. And then inside of the unauthenticated, we can go ahead and add a div and render sign in inside. And let's give this a class name of flex, flex column, items center, justify center, and minimum height of screen. And there we go. As you can see right here, we have our sign in component. Let's see what the error is about. The sign in component is not configured directly. The slash route is not a catch all route. It is recommended to convert this route to a catch all route. All right. So I'm going to go ahead and explore a bit how this, why this is happening. But I think one reason might be is because we are using this. So I think if I switch to React to clerk react, I think that now, yeah, now I don't have that error. Yeah. So perhaps this could be the fix because I think that if we're using the clerk next JS implementation, it expects a different kind of structure, right? I think it expects to be in, as you can see here, a catch-all route, something like this, right? So let's go ahead and use Clerk React instead for this specific scenario like this. Great. So now you can go ahead and log in into your app. So I'm going to click continue with Google here. And I'm going to go ahead and log in now. Let me just, and once I log in, there we go. I can now access my app again. Great. So let's go ahead and add the ability to log out so we can test this out multiple times. So I'm now going to go inside of source. I'm going to go inside of my app folder, home, navbar. And what I'm going to do is instead of using this empty div, I'm going to use the user button component. And we can import user button from add clerk next JS like this. So now there we go. We have the user button of the currently logged in user. And as you can see, we can do a lot of things here. We can add additional email addresses. We can update our profile picture. We can even work with security here. And we can, of course, log out from here and we will be redirected back to the login page. And we will no longer be able to access any page at all. So if I try to go to slash documents 123, for example, you can see that I'm not permitted from doing that. So what I want to do now is I want to implement a full screen loader. So let's go ahead and quickly do this before we end the chapter. So instead of source components, I'm going to add a fullscreenloader.tsx. I will create an interface fullscreenloader props to accept an optional label and an optional class name here. I will export const fullscreenloader like this. And I will immediately destructure these props. So full screen loader props. Let's get the label and the class name here. And then very simply inside of here, we're going to add a minimum height of screen. We're going to add flex, flex column, item center, justify center and gap of two. And then inside of here, we're going to add a loader icon from Lucid React. and we're going to give it a class name of size 6, text muted foreground and animate spin. And then below that, if we have a label, we are simply going to render it. So add a paragraph here, label. And let's give this paragraph a class name, text small and text muted foreground. And to enable this class name, well, we actually won't be needing the class name. So we can remove it. There we go. So that is our full screen loader. And we can now go back inside of our app folder. My apologies. Components. Convex. Client. Provider. And in the outloading, we can now use the full screen loader like this. From dot slash full screen loader. And we can give this a label of outloading. So every time we have a loading screen, we will know exactly why it's loading. In production, you can change this, you know, to a more descriptive message. But there we go. Now it says outloading. So I'm going to go ahead and log in. And there we go. I am logged in. I can refresh the page and you can see that I do outloading again. But since I'm logged in, I'm allowed to stay here. Great. So you can now play around a bit with Clerk. You can explore the documentation. What we're going to do in the next chapter is learn how to use clerk inside of Convex API right here, because there is a way for us to check whether we are logged in in this types of queries in Convex. Nevertheless, great, great job. now let's go ahead and let's use our authenticated state to actually create a new document and properly populate our schema because remember we added some changes and now we have required things like owner id and before we do that i quickly reviewed this little issue we had instead of components convex client provider. If you used the sign in component from clerk next JS, which I would prefer if we use simply because we explicitly installed that package. So let me just log out so we can see the problem. We have an error here and they actually give you the solution. And that is that we can convert this component to hash based routing by setting the routing prop to hash. So let's go inside of convex client provider and set this routing to be hash. And once we change that, no errors. So let's confirm in here, no errors. Let's confirm in here, no errors. Great. So we can now freely log in inside of here and everything is still working. Great. So what I want to do now is I want to create my first kind of post method, right, for creating a new document. So let's go ahead and do that. Inside of convex, inside of documents, we already have a get method. And now let's go ahead and let's export const create method. So instead of being a query, this will be a mutation from dot slash underscore generated slash server. And now whenever we are creating a new document, we're going to have to pass some arguments. And we're also going to have to have this asynchronous handler. So the arguments for creating a document will be a title, which will be optional as an argument, of course. And we also have to import V from convex values. So import V from convex values like this. So besides having the title, we are also going to have initial content, which will have the exact same optional state like this. There we go. So those are our arguments. and then inside of this handler, we can destructure them. So we can have the context and we can have the arguments in the second argument like that. And now we can check whether the user is authenticated or not by using const user, await, context, out, get user identity. If there is no user, we can throw new convex error or you can just either throw like a normal error or you can throw a convex error from convex values. And inside of here, I'm going to pass an unauthorized. Like this. And then let's go ahead and create our document ID here. Because why am I calling this document ID? Well, because when we do context database insert, what returns from here is the ID, right? So let's go ahead and insert inside of documents title. If we have our arguments.title, then we're going to use that. Otherwise, we will use untitled document. And the owner ID will be user.subject. And the initial content can simply be passed here, which will be arguments initial content like this. and then we just return document ID. Or we could have simply just returned this entire mutation here. Like this. There we go. So that's how we authorize with clerk and convex inside of our backend here. So what we have to do now is we have to go inside of our templates gallery and create this document. So I want you to prepare your convex dashboard. So let me just go ahead and see, do I have the dashboard somewhere? Okay, I will just go back here and log in. And I will go inside of my docs tutorial here. So right now I should have no documents here. And now I'm going to go back inside of my Create Next app. And let me just close everything else since we're no longer going to be needing it. And now let's go back inside of our source at folder home templates gallery right here.\nAnd let's do the following. Let's add const router from use router from next navigation like this. I will just move this to the top here. Then let's go ahead and let's add const create to be use mutation from convex react. Let me move that to the top here as well. And use mutation will call the API from dot dot slash dot dot slash dot dot slash convex underscore generated API like this. So exactly like we did in our homepage, API documents get, we are doing the same thing for mutation API documents create like this. And then let's also create a is creating and set is creating use state from react with the default value of false. So let me just move this here. At this point, you might be wondering, can perhaps this be combined with React query? It actually can. Yes. If you go inside of the documentation, you can find the instructions. Let's see where exactly is it. I'm not sure, but they do have instructions. there we go, 10 stack query. As you can see right here, you can combine use query with convex query. If you want to, you can explore it after you finish the tutorial. I would highly recommend that you follow at the moment exactly how I am doing it. But in case you saw these two values, which we have to control ourselves and thought to yourself, oh, use query will handle that for us. Well, good news. They actually do combine with use query, but I will be doing it this way for this project. So let's continue developing this now. Let's create const on template click. Let's accept the title and initial content like this. Let's set is creating to be true. And let's call create and let's pass in the title and the initial content like this. Then, when we successfully create this and get the document ID back, we can call router.push slash documents document ID. So we can immediately start editing that document. And in the finally method, we can set is creating to false. now let's go ahead and assign the on template click here to the button right here on template click and we are going to pass in in the first argument template dot label and the second argument for now just an empty string because we actually did not create anything inside of our templates in regards to initial content. So now, if you go ahead and refresh this and you click on any of these, you can see how they are disabled for a second and then you get redirected to this page and your URL is actually consistent of the ID which you can find right here in your documents table. So we have successfully created a new document with empty initial content and with an owner ID and the title of blank document right here. Excellent. So we successfully did that. What I want to do now is I want to find a nicer way of rendering those elements right here, because right now they're just blindly displayed like this. And then later, we're going to go ahead and actually, if you want to, we can do this now, but it doesn't really make sense to do it now. I'm talking about this, the second argument in on template click, which we've set to be an empty string. Let me go ahead and add just a second. I'm trying to write this. Okay. I'm going to add a comment here to do. And I'm just going to write add proper initial content like this. or maybe can I just do it here? Okay, I used incorrect syntax here. There we go. So add proper initial content. But for now, you will have to pass this as an empty string here. Okay, so now let's go and focus back on our source app folder home page right here because what we ought to do next is properly display these documents right here. So in order to do that, we're going to make use of Shatsy and Table component. But just before we go there, let's go ahead and finalize our convex documents here. So we can use the get method already. But since this will be a list of all of our documents, it's a good idea to start paginating this immediately. And thankfully, with Convex, this is very, very simple. What we have to do is we have to import pagination options validator from Convex server. And then in the get query, we're going to add arguments here. And we're going to simply add pagination options to be pagination options validator like this. So let's go ahead and do the following. And we have to modify this query now to not end with collect, but instead with paginate and pass in the arguments. So we also have to extract the arguments from here. Arguments dot pagination options. There we go. And now in here, we start having some errors because this is now not compatible. That's because we have to modify instead of using use query, we will be using use paginated query like this. So let's set use paginated query. And inside of here, we can skip the first and we can go into the second one, which is initial number of items to be five. So what is this? Well, this is for any other arguments you might have, which we will have later. For example, a search, right? So if we had that, we would add that here. And the last one is reserved for pagination options. So this is the proper syntax. That's why this is empty for now. And instead of just directly having documents, we will now be having results, status, and load more. So all of those things, and we can destructure them like this. So they're more readable. We can remove this for now. And now let's go ahead and do the following. We're going to go ahead and remove this. And instead, we're going to render the documents table, which we have to create. And let's go ahead and pass in the documents here to be the results. Load more to be load more and status to be status like this. Now let's go ahead and let's create our documents table. Instead of home, new file documents dash table dot DSX like this. I will export cons documents table. Return div documents table like that. And let's go ahead and let's create an interface documents table props. So first of all, documents will be a array or undefined. And now let's be more specific. An array of what? Well, an array of doc from convex generated data model. More specifically, doc documents. So now we get the initial content, room, organization ID. Basically, we get our entire schema. So yes, Convex is also fully typed. So you just have to go and target your Convex generated data model from here. So that's the documents. Load more will be a method which accepts the number of items, which is a number and returns a void. And status is a type of pagination status from Convex React like this. Now that we have this, we can go ahead and assign these props here, and we can destructure them. So documents, load more, and status right here. Now let's go back inside of the page, and let's import documents table from .slash documents table. And since we have added the proper types, you can see that there are no type errors whatsoever inside. Now let's go ahead and let's import everything we will need from the table component, which we also have already from chat CNUI, because initially when we started this project, we added all components from here. So we have table, table body, table cell, table head, table header, and table row. So let's go ahead and start developing this. So now you should already be seeing the text documents table here. And now we're just going to center it a bit by first limiting how wide this can even go like this and next auto so already it should not expand as it used to then let's do px 16 py of six flex flex column and gap of five there we go now you can see it is directly aligned with this right here so i'm not counting the arrows because yes technically this container is wider, but these arrows, I think they visually should not be counted, right? I think this looks nicer to align it with the first document like this. So now let's go ahead and first check if we are loading. So if documents is undefined, that means that we are loading. So let's go ahead and fix this by adding loaded. And let's add a loader icon from Lucid React here, which I'm just going to add here like that. And let's give it a class name, animate spin, text muted, foreground, and size of 5. And let's give this a class name of flex, justify center, item center, and a height of 24. So now you can see that for a brief second, well, it looks like I can't really reproduce it now, but for a brief second, we're going to be seeing this loader right here. Great. Otherwise, we're going to do the following. We will be using our table component. Inside of the table we going to have a table header like this and then we going to add a table row the table row will have a class name of hover background transparent and border none i probably should have written this a moment later because i want you to see how it looks like so let's go ahead and just add table head first name for example So you can see how when I hover, there's a certain background color. And there's also a bottom border here. So that's why I added to the table row those class names, because I don't want it to behave that way, right? I want it to be almost invisible. Great. So we have the table head now. For the second one, we're going to add an empty white space. For the third one, we're going to say shared and then created at like this. So this is going to be our table rows here. This will indicate the name of the document. This will indicate who is it shared with in form of is this your personal document or is this an organization by the document. And lastly, we're going to have created at to see when the document was created. And now let's go ahead and let's do the following. Outside of the table header, we're going to check if documents.length is equal to zero. In that case, we're going to open a table body here. Inside, we're going to add a table row and give this a class name of hover, background transparent, for the same reason as we did here. And add a table cell here. No documents found. and let's go ahead and give this a call span of four and the class name height of 24 text center and text muted foreground and then if we have more than one document we're going to go ahead and add a table body here and just say documents so right now we have documents here but if i go into my database, for example, and if I remove this document from here and then go back, you can see that now immediately it says no documents found. So that's the cool thing about Convex is that it works reactively in real time. So no matter which computer, which user from my organization deletes a document, I will immediately see that update here without refreshing my app. And same is true for creating a new one. You can see how it was immediately created before we were redirected here. Great. So in order to display a document, we're going to go ahead and do this documents.map. We are going to get the individual document here, and we're going to render a document row component, which we are yet to create. So the key attribute here will be document underscore ID, and we're going to pass in document prop. So now we have to create the document row component. Let's do that in the same folder structure here. Document-row.dsx like this. And let's go ahead and prepare by importing table cell and the table row from table. Let's create an interface, document row props. document row props will accept a document which is a type of doc from generated data model of documents like this and then let's go ahead and export const document row here let's destructure our props document and let's go ahead and return a table row element here like this then we can go back inside of the documents table and we can import the document row from dot slash document row like this. So now you should well see nothing if you because we didn't really render anything yet. So let's go ahead and first just give it one class name cursor pointer like this. So now well we still can see anything because we still have to expand this document. So let's add a table cell here with a class name of with 50 pixels because inside of here we are going to render an icon as a Google Docs and we can import this from React icons. So let's go ahead and import SA Google Docs from React icons SA. And let me just see, did I do this correctly? SA Google Docs like this. with lowercase d there we go and let's give this icon a class name of size six and let's give it a fill how about we do blue 500 and there we go we now have our first look of how this table cell will look like and now let's go and create another table cell here and let's give this one a class name of font medium. And on MD devices, the width will take 45%. So inside of here, we're going to render document title. So there we go. Now this says blank document right here. After that, we're going to have another table cell. This table cell will do the following. It's going to check for text muted. It's going to have text muted foreground. It will be hidden on small devices and it will be then visible again via flex attribute on medium devices. It will center the items inside and have a gap two inside. And now what we're going to do is the following. We're going to check if document has an organization ID. In that case, we're going to render a building two icon from Lucid React, which will represent, okay, this is a shared document. You have to be careful, right? Otherwise, we're going to import a circle user icon from Lucid React. So make sure you have both of those here like that. And both of them will have the same class name, which will be size 4 like this. So if you want to, you can collapse this like this. So that's the first thing we are going to do. And then just below that, we will check again for the organization ID. and we're going to say either organization or personal like this. So there we go. Since I created this document and not an organization, it says personal and it uses the circle icon. And let's go ahead and do the last thing here. With a class name, text muted foreground and hidden and MD table cell. like this. And inside what we're going to do is we have to display the creation time, right? What I want to quickly check is if we have date FNS installed, we do have date FNS installed because of chat CN. And I believe that if I search for date FNS throughout my project, okay, perhaps some peer dependency uses it. In case you don't have date FNS, you can install it mpm install date fns use the version i have 4.1.0 legacy here depths right but if you confirm that you have it no need to do that so what we can do now is we can import format from date fns like this and inside of this table cell we can do format new date document underscore creation time like this. And let's use the following format. Three capital M's, two lowercase d's, comma, and four lowercase y's like this. And now we should have, there we go, November 27th, 2024. There we go. And now we will add last table cell here, which we will wrap up in the next chapter but for now all you ought to know is that that will be a button so from components ui button like this and inside of that button we're gonna add a more vertical class name from lucid react right here let's give this button some styles variant ghost size icon and let's give it a class name of rounded full like this. And let's give this more vertical a class name of size four. So now you can see that this last row will have a drop down menu, which right now is just an empty button, but it will be a drop down menu. And let's just move this button to the end of this table a cell. We can do that by going inside of document row here. And in the last table cell, add a class name, plex ml auto and justify end. And there we go. Now it's at the end. And we actually don't need ml auto if we are using plex justify end. There we go. And if we go to mobile mode, I believe that one of these will be hidden. Yes, the shared one. So we also ought to hide it in the documents table, I believe here. So shared, we're going to do this class name, hidden by default, visible. Well, can I do flex? Let's see. Okay, this looks just fine. If I zoom in now, there we go. And okay, so we're also going to hide this. So we can repeat that here as well. there we go so this is how it will look like on mobile and then on desktop looks like I've messed it up can I use block can I use table okay let me just go ahead and pause and see what are their default displays I think the default display is table cell there we go so that is working so on desktop we have table cell here. But if we move to mobile, let's use the responsive one. Yeah, it will collapse and it will not be visible. Great. So we did some very good progress here. What we going to do next is we going to add a button to enable pagination Because right now while we do load the paginated query we never really use load more and status in the documents table They are unused. And we also have to wrap up the more vertical dropdown here, which will allow us to rename and delete a document. Great, great job. Now let's continue developing our documents table here by enabling the drop-down menu right here. In order to do that, we're going to replace this with the drop-down menu component. So let's go ahead and create that. So inside of the home route group, we will add a document menu.tsx. Let's export const document menu. and we can just return what we just deleted from the component before it, like this. Then we can go back inside of the document row, and we can import the drop-down menu. My apologies, it will not be drop-down menu. It will be document menu from .slash document menu. So make sure you properly import that. you can remove the unused button in the document row component and the unused more vertical icon. And now we also have to pass some props to this dropdown menu. So let's go ahead and do this. Let's pass in the document ID to be document underscore ID. Let's pass in the title to be document.title and let's pass in the on new tab to the on new tab click and we can already develop the on new tab here so let's go ahead and do it const on new tab click will accept an id of the document and what we're going to do is window open slash documents id and underscore blank like this. And now we have to go inside of the documents menu and let's actually create the interface and props for this. So interface document menu props will accept a document ID, which is a type of ID which comes from documents like this. So make sure you add this import, the same one as we've been using for doc, right? And now we also have the title here and we have on new tab, which is a function which accepts the ID. And it returns a void like this. So let's go ahead and destructure those. So document ID, title and on new tab. There we go. So now let's also import everything we're going to need from the drop down menu. so drop down menu menu item menu content and menu trigger let's wrap the entire thing inside of a drop down menu like this and let's wrap this in a drop down menu trigger and give it an as child props so we avoid hydration errors because drop down menu trigger is a button so what would happen is that we would nest a button element inside of a button element, and that causes hydration errors. So we give it an as child prop, which then enables it to become this button. And then let's add a drop-down menu content here. And then inside of here, we can start adding some items. And the first one we can already create is the open in a new tab item. So let's go ahead and do this. On click here. We are simply going to go on a new tab and pass in the document ID like that. And let's add the external link icon from Lucid React. So make sure you have added this import here. And let's give it a class name of size 4 and MR of 2. So now, if I refresh this and click here, there we go. It says open in a new tab and it will open the document in a completely new tab. There we go. Also, I think that maybe you actually can do this with router. I'm just not entirely sure how. I mean, if you want to use the router from user router navigation. I just did a quick search. I paused the video and actually I did not find any good way of doing that. So this solution is completely fine. Okay, so we confirmed that our document menu drop-down item to open in a new tab is working as intended. So what I want to do now is I want to create the delete functionality. So for that, let's quickly go back inside of our convex documents. And let's add export const removed by ID to be a mutation. And the arguments for D will be ID, which is a VID documents here. And then we're going to have a handler, which is an asynchronous method, which has context and arguments like this. So the first thing we're going to do is check if we have the user using await context user. How do you do it? I already forgot. Context out to get user identity like this. If there is no user, we can throw new convex error, unauthorized. Just a reminder, you can also throw normal errors if you want to for whatever reason. And now let's go ahead and let's actually remove it. So first thing we're going to do is we're going to fetch it to see if we are the owner of that document. So context database get using arguments ID. If we cannot find that document, we will throw new convex error document not found like this. And now let's check if we are an owner using document owner ID equals user subject like this. If we are not an owner, in that case, we are going to throw new convex error here, unauthorized. And finally, await context database delete arguments ID. There we go. So what we have to do now is we have to create a reusable remove dialog. Let's go ahead and do that. I'm going to go ahead and create this instead of source components. and I will create a removeDialog.tsx like this. Inside of here, I'm first of all going to mark this as useClient. And then I'm going to import alertDialog, alertDialog action, cancel, content, description, footer, header, title, and trigger, all from components, UI, alertDialog, which we've added via chatCNUI. Let's create an interface remove dialog props to accept a document ID which is a type of ID from ././.convex documents like this and children which is react react node. let's export const remove dialogue like this let's get the props remove dialogue props let's get the document id and the children right here inside of here let's go ahead and let's return the alert dialogue like this let's add the alert dialogue trigger component and give it an as child prop and simply render the children inside so we can turn whatever button we want into a remove dialogue trigger. Then let's add the alert dialogue content here. Let's go ahead and give it an on click event, event stop propagation. This will be quite useful because most of the time we'll be opening this kind of dialogue inside of a drop-down menu and inside of an item which by itself will be clickable later on. So this will later redirect to something. This is good. I forgot that we have to add the redirect. But basically, we have to add the stop propagation to make it behave normally. Now let's add the alert dialogue header inside of the content and let's add the alert dialogue title here asking are you sure below that add the alert dialogue description with a more descriptive text for example this action cannot be undone this will permanently delete your document something like that outside of the header add the alert dialogue footer component and inside of here add the alert dialogue cancel make sure you close that and inside simply add the cancel text and let's go ahead and do an on click here as well and simply stop propagation here as well because that's one another clickable element here and then inside of here we're going to add the alert dialog action and this one will say delete like this. Great. So before we develop this further, let's actually see how this actually looks like. So we're going to go back inside of our document menu here. And now what we're going to do is inside of this dropdown menu content, we're going to add our remove dialog from components remove dialog. So I'm just going to move it here alongside with the button here. And now the remove dialogue will do the following. It will be a drop down menu item. I mean, the children of the dialogue will be a drop down menu item here. And it will have the remove text inside and trash icon from Lucid React. And let's give this icon a class name of size four and MR of two. Now we also have to pass in the document ID to be document ID here. There we go. Make you have imported the trash icon from Lucid React. And now you can see that we have the remove option. And when we click on it you can see that it briefly opens the alert dialog and then in the block click closes That because we are opening it inside of the drop menus And that why we took extra care for this propagation resolves here And we have to do the same thing here. So in the drop-down menu item, add the on select here, get the event and stop the event propagation here. And then do the same thing for on click. So for this drop down menu item here in which the remove dialog is wrapping. So now if you try this again, looks like it's still not working. So let's go ahead and see what else could be causing this. So we did add on select. We did add on click here. Perhaps something else is afoot. I'm going to go ahead and debug a bit. All right, so I think I might have figured out what's wrong. In our on select, I think we should be doing prevent default like this. Let's see if that will resolve the issue. There we go. So once we block the on select on working as it usually works, we can now see our alert warning us that this action cannot be undone. So in the drop-down menu item in the document menu, which is encapsulated by our new remove dialog, the on select should be prevent default and the on click should be stop propagation. Now let's go back inside of the remove dialog and let's actually develop this. So I'm going to add a remove method here called the use mutation from convex react. So I'm just going to move it here. And inside of use mutation, we should be calling API. API from convex generated API. Documents.removeById like this. And if you want to, you can also add is deleting or is removing, set is removing, use state from React and false by default. So let me just move this here like this. And now what we can do is the following. We can go ahead and disable the alert dialog action. If is deleting like this and then on click we can get the event and my apologies is removing. First of all, let's stop propagation and then let's set is deleting. Set is removing to true. let's use the remove mutation passing the id to be document id go ahead and get uh well we actually don't need them we just need finally here and on finally set is removing back the false like this and i think this should work just fine let's go ahead and try out. So right now, I'm going to go ahead and select remove, delete. There we go. No documents found. But if I click, it creates a new one. Let's go ahead and try and create a few more, just so we have them here. All of them have different names. So that's great. Let me try and remove the project proposal. There we go. You can see how this immediately updates our UI. That's so great about Convex. Great. So that is working. And now we're going to create a similar thing, but for updating, or I mean, more specifically, renaming our document. So let's go ahead and let's develop the update by ID method. So it's going to be similar to remove by ID. So we can copy it and we can paste it here. So let's call this update by ID. same arguments here same logic here document not found we are not the owner and then finally what we're going to do is we also have to extend the arguments with a new title so that will be without string like this and then await context database patch and simply pass the new title and that is it that's all we have to do so arguments.title like this there we go that's our update method And now we have to develop the rename dialog. So it's going to work similarly, but also a little bit differently. So let's prepare by copying our remove dialog and let's rename this to rename dialog. Now the fundamental difference here is going to be the type of component we're using. So in the remove dialog, we were using alerts. But now in the new remove dialog, we're not going to do that. We're not going to be doing that. We're going to be importing dialogue, dialogue content, description, footer, header, title, and trigger like this. So let's go ahead and let's rename this from remove dialogue to rename dialogue. Be careful, they're similarly named. So make sure you are building this inside of the new rename dialogue like that. So instead of using the other dialogue, well, I think it will be easier to just remove the whole thing here. So let's add our dialogue here like this. Then let's add a dialogue trigger with an as child prop and render the children inside. That will stay the same. Then we're going to have the dialogue content option. And we're going to have a native form element. Then inside of that, a dialogue header. Then a dialogue title. Rename document. below that we're going to have a dialogue description which will be a bit more well descriptive enter a new name for this document for example and then we're going to go outside of the dialogue header and add a div with a class name my4 and inside of here we are going to render an input from .slash UI input because we are in the components folder. So no need to go to components, right? Because this is shorter in this case. So yeah, if you want to, you can also use the dialog like this, however you prefer. Okay. Now for this input here, we're not actually going to make any drastic changes here. So we're just going to add later some attributes here. And still inside of the dialog, still inside of this, my apologies, outside of this div, we're going to add a dialog folder like this and we're going to add a button from UI button like this so I will just move it here with the input and then inside of the button we are going to say cancel and then another button where we are going to say save like this so this is the skeleton of our rename dialogue. Let's go ahead and let's use it. So we're now going to go back to our document menu inside of app folder home document menu here. And we can copy this implementation of remove dialogue inside of the drop down menu content. And this time use the rename dialogue like this. so we imported the rename dialogue here i will just move it here alongside the remove dialogue and inside of here we're going to say rename and we will be using the file pen icon from lucid react so make sure you have added this icon here and make sure you do on select prevent default and that you do on click stop propagation make sure that you have wrapped this in a rename dialogue and not accidentally twice in a remove dialog. And you also have to pass in the document ID. And I think that now, if I go ahead and click Rename, I should be seeing a Rename document model right here. So now we actually have to add some other properties here, starting with that the Rename dialog should have the initial title here, which will be our unused title prop. There we go. So let's go back inside of the rename dialog and let's actually modify the props a little bit. Alongside document ID, it will accept the initial title, which will be a string. Then we can destructure that here. And then we can modify these things a bit. Instead of remove, this will be update and it will call API documents update by ID. Instead of is removing, this will be is updating and set is updating. Then let's go ahead and let's set the title and set title and use state initial title like this. Let's also define open and set open here to be used state and false by default. And then we're going to go ahead and give this dialog an open of open and set, my apologies, on open change to be set open. So we are both going to be able to programmatically control it. Like for example, when we submit, we can call set open and change this to false. And also this trigger will call on open change and set it to open. So just because we set this to false now and assign this props, it's still working normally. Let me show you. There we go. It still opens because this part will trigger this part. But we can now also programmatically decide when this will be set to false. like for example on submit now let's add an event react form event here and let's add html form element let's prevent default let's set is updating to true let's call update let's pass in the id to be our document ID and our title to be title.trim or untitled if we have decided to remove the entire input. And then let's go ahead and chain.finally here. And let's do set is updating set to false. And let's also close this. There we go. And now we can assign on submit to the form here. Now let's see if we have to style something.\nor maybe add some other things. For example, in this input, we are missing a lot of things. So the value of the input will be the state-controlled title constant. OnChange will get the event from the OnChange and simply call set title to be event target value. Placeholder will be document name. OnClick here will take the event and do event stop propagation here. Our buttons will do the following. The cancel button will be a type of button. Variant will be ghost. Disabled will be is updating. On click will take the event, stop propagation, and set open to false, like this. Our save button, on the other hand, will explicitly have a type submit. disabled if is updating and on click which will take the event and stop propagation as well and i believe that is it i think this is all that's necessary for our rename to work so let's go ahead and try it out so i have one document called resume and one project proposal if i click rename here, resume one, two, three, can click save. There we go. It becomes resume one, two, three. And there we go. That's it. And one thing that I guess we can kind of change is this set open set to false. I would rather that only happens on then. Why on then? Well, because in case there's an error, I want, you know, I think it's kind of a better user experience, If we have an error, don't close it. But you can decide for yourself. If you want to, you can put it inside of finally if it's simpler. There we go. So we have that now. So what I also want to do before we wrap up is the following. I want to go back inside of my home document row here. and I want to implement the onClick of the individual table row here. So let's go ahead and let's add on router, useRouter from nextNavigation. So make sure you add an import for this like that. And then I'm going to do const onRowClickID, which is a string like this. and what we're going to do is router.push slash documents ID and simply assign the onclick here, onrollclick and pass in the document. Oh, now that I look at it, no, there's no need for this. Yeah, we can just do this. rather a push back text slash documents and then document underscore ID. Yeah, no need for separate method here. And same is true for on new tab click. I believe we can just do this and use the document. and remove the semicolon there we go so no need to define those methods if you want to you can but we can explicitly call document id here so i guess yeah maybe we in the document menu technically if we have document id we can also do that here yeah so however you prefer if you want to you can keep them in the on new tab click it really doesn't matter but now the table row itself will be clickable. So if I click here, I should be redirected here. So what I want you to do is to confirm that now when it's clickable, you can still do things. You can see we do have some issues. So in my rename, it looks like I can type in an input, but when I click on any part of my dialogue, it redirects me. So it kind of propagates. So I think that we can resolve that, for example, in the rename dialog here by capturing that somehow. Let's see. On click, event, event stop propagation. Let's see if that resolves it. So if I click on rename, there we go. I can now click around in my document and it will not redirect. Looks like it eventually did redirect, but that's okay. I guess I clicked outside of it. Great. So we have that resolved. And let me just see, on click here does not exist. Okay, so yeah, this is the best we can do so far. Great, so that works now. And I think we also now have the same issue. Do we have? No, we don't have the same issue here. Okay. Let's just confirm does opening a new tab works. So all of this seems to be working nicely. One thing I want to do is I want to wrap up the pagination so we can close this chapter. And then in the next chapter, we can implement the search logic. So let's go ahead inside of our home, inside of page here. So we are using results, load more, and status in the documents table. So let's focus exclusively on the documents table here. And what we're going to do is the following. We're going to go all the way to the bottom here. and we're going to add a div and a class name, flexItemsCenter and justifyCenter. And then we're going to add a button from components UI button. So just make sure you have added that here like this. And this button will do the following. It will have a variant of ghost. It will have a size of small. it will have an on click which will call load more and now the number of items to load will be five and it will have a disabled status if status is not equal to can load more now i'm going to collapse all of these attributes for you a lot of you ask me why don't i use just why don't i just use prettier to do this for me uh i don't like using prettier uh while i do tutorials i use Prettier on my own. But while I do tutorials, what can happen is that Prettier can fix something that's outside of the screen, which is currently recorded, right? So that's why I don't use Prettier in my tutorials. And now let's do the text inside. So status can load more. In that case, we're going to say load more. Otherwise, end of results like this. So load more. We are loading by five. If you want to, you can separate this into a constant to be a batch size. And what's important, if this is five here, you should also have initial number of items five here in your paginated query. So right now you can see that this says end of results. So I'm going to go ahead and play around and create a bunch of these documents so that we can actually load more. Let me just fill them up as much as I can. Let's go back to the root page. And there we go. You can see that it only says, it only displays one, two, three, four, five documents. And when I click load more, there we go. It loaded the next one. And then it confirmed that this is the end of results. Nothing else to load. Perfect. So we also have a pagination result here. So what we're going to do next is we're going to enable the search here. And then we can finally go ahead and learn how to store what we type here in each of this document ID. We can then finally enable the rename and remove from here. What you also have to do is enable renaming from this little util here and add collaboration. And also, we have to add organizations. And that is it. You are doing absolutely amazing. Great, great job. Now that we've added pagination to our document table, as well as enable some useful pop-ups like rename and remove, let's go ahead and let's develop the search functionality. So when I search, for example, resume inside of this search bar above, I expect only resume to pop up here. And when I clear this, I expect all of them to appear once again. So we already have half the work done for us. Thanks to this index, we've set in convex schema.ts. So inside of here, we have added a search index. And we can also filter this search field specifically inside of one owner ID or one organization ID. So we don't have to go through every single document inside of our database, which is a much safer and much more optimized query. So let's go inside of convex documents.ts and we'll have to modify our get method here. So besides accepting the pagination options, we are also going to accept search. Now, this search can, of course, be named whatever you want, but I think it's self-describing if we call it search. So it's going to be a type of optional because it will not be required. And more specifically, it will be an optional string. And now what we're going to do is the following. I am going to run separate queries depending on if we have search and if we don't have search. So this is what I will do. First of all, I'm going to protect this route because I just noticed this route is not protected. So let's await context out, get user identity. And if we don't have the user, we are going to go ahead and throw new error or convex error, which will be unauthorized. and then let's go ahead and see if we have search so if we have search i'm going to return a specific different query here so await context dot database query documents with search index search title and then i get the query inside of here and i will do query.search title and i will pass in the search like this. And my apologies, we have this from arguments.search like this. So arguments.search. So we resolve this for the, oh, is it search? It is. Okay. And now what we also have to do is dot equals here owner ID and make that user subject So we are only searching for this inside of this owner ID But we do seem to have a little error here i will just move this right here let see what the error here so string or undefined is not assignable to parameter of type string undefined is not assignable to type string So let's go ahead and see how we can resolve this right here. So search is an optional string here. Perhaps there's something I can be more specific about in my schema here. So search field is title, owner ID. Oh, so if arguments.search, I thought that should be enough for this to be protected. But for some reason, it is not. um i'm not sure why this doesn't work but let's try destructuring the search in here immediately and then check for search like this oh now it works okay that's interesting i don't know if this is some javascript thing that i'm not aware of or maybe just a convex type thing feel free to correct me in the comments if you figure out what it is. Maybe I'm not seeing something very obvious here, but yeah, you can just destructure search directly from the arguments then. And from here, you also then have to add pagination options as a destructure thing and remove the arguments dot pagination options. There we go. So now you should have no errors here and you can double check that your npx convex dev has the last message convex functions already. There we go. So now we have enabled the special if clause if we pass a search. So what we can also do now is also improve this query because right now we're just paginating everything. And one thing we forgot to do here is also paginate this, right? So what we're going to do is we're going to chain dot paginate here and pass in the pagination option. So yes, we can chain pagination with searching as well. And it's as simple as this. Keep in mind that in this query, I'm immediately returning this, right? So don't accidentally wrap this inside of curly brackets, because then this doesn't return anything. You can see the error. You have to explicitly write return. So you can do it like this if you want to, or you can just not use curly brackets like this. So let's do the same thing here. So what we are missing here is the following. We just have to add with index here. And let me collapse all of this here. So await context database dot query documents, then we're going to do index and then we're going to paginate. So with index is going to be by owner ID like that, which is not in their own parenthesis like this by owner ID, then we get the query. And then we're going to do query equals owner ID to be user dot subject. There we go. So now both our non-search query and our search query are protected with both the owner ID only and owner ID here. So right now, nothing should change. If you created all of these accounts in one account here, you should be able to only see these accounts here, right? You shouldn't see anything different. But if I, for example, log out now, and if I log in into a different account, you should see that now I have no documents here, right? Because I'm now in a different account. So I can create a new document here, and it's going to be the only document I have. And then if I sign out. And if I go into another account of mine here, I can see all of my older ones. Great. So it should work the same for you. And that's all thanks to this new width index, which we have added here. One thing we did not try is the search. And that's because I think it should not work yet. Because what we have to do is we have to go inside of search app home page.tsx right here. and we have to pass the search inside of here and we can get the search by obtaining our use search param from hooks use search param so let me just move that here if you remember our use search param is hard-coded to look for the search uh inside of our url using the nux package so we have already used this inside of our search input in home right we've used use search params and in Here we use the set search. And this time we just need the search. So I can go ahead and pass it here, search. And you can see that it accepts that as an argument. So let's try it out now. I'm going to refresh and I will try and search for resume. And there we go. It only gives me resume and resume one, two, three. And if I clear everything, there we go. It goes back. Perfect. So that seems to be working. The pagination seems to be working. and we are done with search. What we got to the next is add organization. So you can create a document inside organization and not for your personal account. And then when we have organizations, we will be able to add collaboration for those people within that organization. Now let's go ahead and let's implement the organizations feature. And we can do it quite easily thanks to clerk. So head to your clerk dashboard for your project, right? So in your clerk dashboard, once you log in, make sure that you are in the project where we configured our keys and all of those things. Go inside of configure and find organizations, I believe, or maybe just let's use the organizations tab here and click enable organizations. So it is in configure organization management settings and click enable organizations. There we go. So now organizations have been enabled for our project. And if you want to, you can also explore some permissions, roles, and everything else that the settings tab offers. But for our project, this will be more than enough. Do not close this tab yet because we will have to do something with our JVT templates in a moment. So for now, I want to add the organization switcher component, which again, thanks to clerk is not something we have to even develop ourselves. We can go inside of source, app folder, home, navbar. And just as we've imported user button, we can now add organization switcher component like this. And then we can go ahead and do this. Let's wrap this user button inside of a div. Let's give it a class name of flex, gap three, and items center. And let's add organization switcher component here. Like this. And there we go. As you can see, we now have the organization switcher here. I just want to confirm how this behaves on smaller devices. So I do want to leave a little bit more space here. So what we can do is we can also add PL6 here, I believe. There we go. Now this part has a bit more space. Great. So how does the organization switcher work? Well, by default, if you didn't create any organization, you can see that we only have our personal account. And then you can click create organization here and you can upload an image for your organization or you can create a new organization. For example, Code with Antonio. Let's say that's my new organization here. And I can go ahead and invite members, for example. So I'm going to go ahead and invite the user on my other profile. So remember, I have two accounts here. So I'm going to go ahead and send an invite. So I'm going to pass my email here. And I can specify the role, member or admin. So I'm going to leave it as member. And I will click send invitations here. And there we go. Invitations successfully sent. So right now you can see that I am within this organization. If I want to, I can switch back to my personal account. And you can see how every time I switch organization, this is why I asked you to put that full screen loader here inside of source components, convex client provider, right? I asked you to write the label out floating. So this way, you know, every time that out is being refreshed. and this is very good for us to know that every time we switch an organization the jvt token from convex is being updated which means that we will be able to read the new organization on convex backend and that's why it's also important that you're using this convex version convex 1.17.3 of course this is the minimum required version if you're watching this in the future and you're using a higher version and everything works fine no problem no need to downgrade but don't use a lower version, like 0.2. Make sure you're using at least 0.3 here, because that's when they've added that very cool feature that out is being synchronized with new organizations. So now what I want to do is I just want to sign out and I want to log in into my other account here. And also, by the way, that invite, which we have sent also got into the email of this account. So Yes, we actually received an email, but besides an email, I've also gotten it right here. So you can see I can press join to that organization. And now I can switch to that organization in my other account here. And I can go ahead and click manage here. And I can see everyone else who is a member inside of here. I can see who is an admin. But since I'm a member, I can't really kick users, but I can leave the organization myself if that's what I want to do. Great. So I would recommend that you have at least one person inside of your organization. I mean, at least two people inside of your organization so that you can test out the collaboration feature. But what we have to do now is, well, fix this, right? Because right now, in my personal account and in my organization account, you can see that I am seeing the exact same documents here. So there are a couple of things we have to resolve. Well, first things first, this is what I want to change. I want to go inside of my app folder home navbar, and I want to add a couple of props here. So every time after we create a new organization, I will redirect to the root page. After we leave the organization, I will redirect to the root page. After we select an organization URL, I want to go to the root page. And after we select the personal URL, I want to go to the root page. What this does at least in this version which I have let me see my clerk version 6 what this does is it will always cause a refresh of my page which is also great because it further ensures that we get a new JVT token every time we change an organization So both the convex is subscribed to the change and also we are causing a refresh here. And this is also quite useful because later if we add more pages and if we are accidentally in a document which is supposed to be personal, and then we switch to organization, we should be redirected away from that document, right? We should not be able to still see that document because technically we are now looking at it as an organization, right? Great. So now that we have added this, let's actually go inside of our convex documents. And what we have to do now is we have to learn how to obtain an organization ID from this user. Because right now, here's what I'm going to do. I'm just going to console log my user here like this. This is what I'm going to add. Every time I get my documents, I will console log the entire user that I have here. So you might be wondering, where is that console log visible? Since this is a server, but it's technically a convex server. It's not visible here. It's not visible here. And I can tell you it's not going to be visible here as well. Well, I just told you it's not going to be visible, but I did not expect Convex to be this useful. It actually is visible here, which is great. So they actually give you this feature to see your server logs here. My apologies for telling you it's not possible because it most definitely is. But let me tell you one other way you can do it. So if you go to a clerk dashboard, my apologies, convex dashboard. And if you go into your new project, docs tutorial, in here, you can go inside of logs. And inside of here, you can see all the errors which you might have. And also, for example, this log right here. And let's take a look at this log. What we can notice is that while we have the subject, which is the user ID, we don't have any information about the current organization ID of that user. So what we have to do here is the following. We have to go inside of our clerk dashboard. We have to go inside of our JVT templates, select the convex template here. And then inside of here, we can add some new claims. So go to the last one, add a comma and press space. And you will now have an error here because it's a trailing comma. but before you click save what you're going to do is you're going to add organization.id like this and you're going to map that under organization underscore id like this and there we go no more errors so this is the field which you have added you can also manually write it or you can use you know the claim by clicking here so just leave it like this and click on save right here you can also always reset it if you think you've messed it up. And there we go. Once you save, it will be, I think, alphabetically moved to its position. So there we go. Organization ID right here. So now is it the same thing? Let's refresh again. We still have that log and let's check it out now. So I'm going to go all the way down right here. And there we go. You can see that now I have the organization ID, but it's currently null. So let's see what happens if I switch from my personal account to code with Antonio here. I now initiated the get method again, so we'll close this. Let's go down here, and there we go. You can see that now our organization ID is synchronized to our backend, which means that we can safely rely on this method right here to get our current organization ID, and depending on that, we can change the query to only fetch by certain organizations, and we don't have to do anything inside of our schema because we have already added the organization ID field, and we already added the organization ID index here. And we also enabled search within organizations as well. So all we'll have to do is modify this get query, and then also we'll have to modify the create query here as well. So let's go ahead and let's modify our get query. And we're going to start by after confirming that we have the user, we're going to get the organization ID by checking if we have user.organization underscore ID. A bit of an issue, if you could call it that way, is that custom claims don't really have a defined type. You can see that we don't get an error, but it is just set as a JSON value or undefined, right? Because this can be anything, because this is a custom claim, right? This is not what's initially added here. We added it. By any chance, if you're watching this into the future and this becomes the standard here, obviously you don't have to add it. But at the time I'm making the video, the organization ID is not included in the convex JVT template here. So we check if we have the organization ID and if we don't, we're going to mark it as undefined like this. And we also have to define exactly what this will be. So what we can do is we can write as string either string or undefined like this. So when you hover, you can see that organization will be either string or undefined. Not the prettiest solution. Feel free to make a breather one, but it needs to be a string or undefined. Make sure it's not JSON value or undefined and make sure it's not string or null. So it needs to be string or undefined. Great. So what we're going to do now is modify our query a bit. First, I'm going to add if we have search and if we have an organization ID. In case of that, we're going to return await context.database query documents with search index search title. We're going to get the query. We're going to immediately return query search title search. and then we're going to chain equals instead of owner ID, we're going to now look within this organization ID like this. There we go. And of course, paginate with pagination options like that. And then we can just chain this into an else if, even though I think we don't have to do it because both will return and break, right? So I think we can also if however you prefer if you want to you can do it like this if you want to you can chain else if here let me try like this so if we have search and we have an organization we will run this query if we only have search we will run the owner id and then what we will do here is this will run in the end so if we neither have the organization id nor we have the search then we load personal things. But what we also have to do is if we are just loading by organization, so we only have an option to load from organization if we are searching, right? But we need an option to just check for if we have an organization ID. And in that case, I think you can just copy this one, add it here, and change the index to be by organization ID and change this to be organization ID. And this inside will be organization ID. And remember to paginate. So yeah, I think all this will work just fine. So this is search within an organization. This is your normal search. This is if you have no search, but you are inside of an organization. And this is if you're searching through your personal documents. If you want to make it easier for you, you can write a little comment for that for search within organization for example personal search all docs inside organization and this will be all personal docs there we go so let's try it out right now if i'm within an organization i should not be able to see anything if i search for something like resume or maybe blank, right? I should still not be able to see this. Even if I create a new document, I should be redirected here, but I should still not be able to see anything and I should still not be able to search for them. But if I switch to my personal account, I should be able to see two blank documents and I should still be able to search. There we go. Let me just try a different one so I can confirm. I have project here. Let me search for project. There we go. It works, but only within my personal account. If I switch to organization, it's not that it's not working, it's that we don't have anything inside of here. So let's go ahead and let's resolve that. What we have to do is we can copy this constant organization ID and we can go inside of the create mutation here and we can obtain it here. And then we can modify the create method if we have an organization ID. So what we're going to do here is the following. we are simply we can keep the owner id right because still this is the person who created the document and then instead of here if we have an organization id we will simply assign the organization id that's it that's all we have to do so let's go ahead and try it out now make sure you are within an organization and click on new document and you should now see this document and you can see that our document row is working because it's showing organization here. Let me try with project proposal. Let me try with a resume. So all of them are now within my organization right here. And if why is that working? So let me remind you inside of my document row here in this table cell here, we check if we have a document.organizationID, we load the building two icon, which is this one. And using the same logic, we also display organization, right? Let me try searching within an organization, for example, project. There we go. Works perfectly. Let me try clearing this. It works perfectly. And here's a cool thing. If I go to my personal account, this will display both my personal and my organization documents because I am a part of both of them, right? So that's a cool feature here. And you can see how we differentiate between the two with personal and organization. Great. So we just implemented this. And now what I want to do is wrap this chapter up by one thing So right now I noticing that let me try and removing Oh yeah let kind of improve our methods here For example let me show you what you could do So this will depend on you and what kind of logic you want to implement. For example, the remove by ID. It is up to you to decide who can remove a document. If you want to, it can only be the person that created the document. Because for example, right now, so two of us are inside of this Code with Antonio organization, right? And I've created all of these documents within this profile here. So I will now log out and I will go inside of my other profile. And while this other user will be able to see them, let me just switch to Code with Antonio here. There we go. So this user can see them. Right now, if this user tries to remove this, you can see that we get some errors because I'm unauthorized to do that. So it's up to you to decide is this the behavior you want or not? Do you want to allow anyone from the organization to be able to remove that? Well, you can do that if you want to, and I will show you how, and then it's going to be up to you how to do it. So if you want to do it, so copy the organization ID definition, go inside of remove by ID, and right here, what you have to do is define the organization ID, And then you can do this alongside is owner. You can also do is organization member like this and check if document.organizationID is equal to the organization ID. And then you can check for both of this. If not an owner and if not an organization member, then throw an error. So right now, even though this person did not create these documents, they are within Code with Antonio organization and they should be able to remove the document. And it looks like they're still not. So let me try one more time. Remove by ID. Is organization member. Did I do this correctly? Organization ID, organization ID. So I think it should be working. Let me try switching between my accounts here. Document proposal remove. Okay, it's still not working. So that's interesting. I'm going to go ahead and debug why it is not working. Well, it's not working because I used the wrong operator here. So this will fail either for a non-owner or for a non-organization member. My apologies. It's supposed to be and-and. So if neither of this is true, then it will fail. I've debugged by adding this console log here, right? And once I've added this console log, I looked into my convex logs here and I looked at, okay, so it's not an owner, but it is an organization member. and then it immediately clicked to me. Oh, I probably used the wrong operator here. So that should now work. If you go ahead and try it again and delete it, there we go. Okay, this time it works. And you can use the exact same logic for your update by ID, for example. If you want every member who is a part of the organization to be able to do it, this is how you do it here. So I will just copy all of these lines here and just add them here. There we go. So now, even though this person did not create any of these documents, they should be able to rename them. And it is possible. Great. If you want to, you can go even more specific. So right now, we are only checking if this is an organization member, right, by checking the organization ID. But let me remind you that inside of this convex JVT template, what you can do is you can add additional custom claims. For example, you can see the role of that user within that organization. Because remember, we have admins and we have members. For example, you want to allow every admin within an organization to delete this. So then you can add an organization role and you would use the same thing here. Instead of getting the organization ID, you would get const organization role, user.organization role or undefined, right? You can already see how you would do that. So for example, you can use that as an exercise if you want to add a third logic here is organization admin. And then you would check for that, for example. But I think most common behavior is that anyone within an organization can rename and delete files, right? So great. We have added that. One thing that I kind of want to do here is that I dislike the errors we have. I think it would be great if we could capture them. So let me go ahead and show you how you can do that because we already have this package installed. So you can go inside of your app folder, your layout here, and you can import toaster from components UI Sonor and you can just render it like this, your toaster. And that's it for the toaster setup. And then, for example, if we go inside of our remove dialog, you can import use toast or toast from Sonner. Yes, just toast from Sonner. So you should already have this installed and you should have the toaster component. That's because we added all components from Shotzi and UI, right? So for example, what you can do in this remove here is you can catch it and you can do toast error. Something went wrong like this. So now what I'm going to do is I'm going to go back inside of my documents dot convex and I will modify my remove logic once again so that only the owner can delete it. So now I should get a nicer error than the one from before. So let me click remove. There we go. So something went wrong. This is a much nicer error. This is still logging the error. But what's important is that user sees this error because they will not see this in production. They will see this. And that's what we want. Great. So I will now revert this change here. And if you want to, you can also, you know, chain dot then here for a toast dot success document removed. Like that. So let's try removing a document. There we go. Document removed. And you can do the same thing inside of, for example, the rename dialog. So I'm just going to copy these two things. Find the rename dialog. I believe we have a update method here in the on submit. So I will just change this too. I will import toast from Sonar. I will move it to the top and that's it. We don't even have to test it. We know that it works exactly as it's supposed to. and then i believe we also did one in the template gallery here so what we're going to do is on template click when we create it uh i'm going to just leave the catch here and for this one i'm just going to copy it and add it here and i will remove this one and we have to import toast from sonar there we go so now we just added i believe a much nicer implementation of notifications here So if I create a new one, there we go. Oh, it says document removed. Document created. And in the rename dialog, document updated. There we go. Okay, so that's what was missing. And before we end the chapter, just one very, very easy thing I want to do. I want to add these features, the organization and the user button in this screen. Because right now, we are missing those things here. So let's do them very quickly. So I'm going to keep my navbar open where I have them right here. And what I have to do is I have to open my navbar from individual document ID here. So we have to go all the way to the end here. And we have to add our, well, this thing, right? Basically just copy the organization switcher and the user button here. Actually, you can copy the div encapsulating them as well like this. and you can find the last closing navdiv and simply add it here like this and import the organization switcher import the user button from clerk next js i will move that here and now there we go you should be seeing both of those here and if you switch an organization it should redirect you back to the documents here great so now you can control your account log in and log out all from different types of dashboards here. Great. So that is all working. We have our search, we have our organizations, we have the pagination. As I've said, ensure that you have at least two users inside of an organization here, because now what we're going to do is we are going to create live collaboration. Great, great job. Now let's go ahead and let's make our documents collaborative. Alongside making them collaborative, let's actually make them store our changes. Because right now, if we type something in a document, regardless if our ID is in the URL, we have no functionality to save what was written. So let's go to liveblocks.io and go ahead and create an account. Once you create an account, you can visit your dashboard. Here in the dashboard, you'll probably have two projects. Choose the development one, and now we're going to follow the steps to install this. As you can see, I have some active rooms because just a moment before I was attempting to install this, and I run into some issues, which we are now going to learn how to resolve. So what I'm going to do is I'm first going to show you where you can find your API keys. So you can find your API keys here. And in here, you have your public key, which you can click to copy. You're going to need that later. Now, I'm going to go into the quick start here. And you don't have to do this with me, though it's probably very useful. But if you're watching this into the future, websites can change, dashboards can change. So no, it's not required that you follow this along as I do. But if you can, if this is exactly the same, go ahead and follow it because it is quite useful. So get started with LiveBlocks. What are we building? We are building a text editor. And if you're interested, you can use LiveBlocks for so many interesting things here. And they have such simple integration, which literally takes you step by step on how to do it. For now, we are going to focus on the text editor. So I'm going to select that. And you can see that they have support for TipTap, Lexicl, Slate, Quill, BlockNote, all of those things, in our case, we will be using LiveBlox tips.\ntap. More specifically, live blocks tip tap for next JS. And this is where I ran into the first problem. And the problem is because of React 19 release candidate. So here's a quick tip. If you are using anything other than NPM, meaning if you're using bun or yarn or PNPM, which also means that in all of these previous chapters, you never had a single issue with installing peer dependencies, right, you never got, you never had to run that flag dash dash legacy peer depths, you will have no issues. But if you're using NPM, and if we are using legacy peer depths, you will run into some issues here, which we are going to learn how to resolve together. So install live blocks and tip top, every live blocks package should use the same version. So I'm just going to copy this, I'm not going to immediately run it. And I will shut down my app here. And I will just prepare the command here. So I'm not going to run it yet. First of all, we don't need tip-top starter kit. We already have that. We also don't need tip-top react because we have that as well. So let me just remove the extra letter here. And now what I want to show you is that the versions of all of this live blocks packages in the time that I'm recording the video are 2.12.2. So what I'm going to do is I'm going to explicitly add those versions. And if you want to do the exact same thing, you can do so. So all of them will be explicitly 2.12.2. And the last one here, 2.12.2, like this. And I'm going to add dash dash legacy peer dabs. So if you're using bun, you will have no issues after this command. If you're using dash dash force, you will also have no issues. But if you're using dash dash legacy peer depths, which is something we have decided to use early on. So it doesn't really make sense to now decide to use force just because it will fix an issue. Instead, we're going to learn how to resolve it. And we're going to talk about why the issue even happens. So now I'm going to go ahead and install all of these packages here. I'm going to show you my package JSON so we can talk about what has been changed. So let's go inside of my package.json here. And you will now see live blocks client, live blocks react, live react tip tap, and react UI right here. Great. I think those are the only changes so far. So now let's go back inside of following this tutorial, right? And now we have to run the following command, which I will again just paste. I will not run it yet because of this at latest. I don't know how far in the future you are watching this, but at the moment, my version is this one. It's quite a long one. So feel free to pause. If you want to, you can use latest. I don't think, you know, any breaking changes will appear, but I do want to ensure some kind of longevity of my videos. So that's why I will be providing you with exact versions that I'm using at the time of building this. So that will be npx create live blocks app at 2.20240816.0 dash dash init dash dash framework react. Let's go ahead and run this command. And there we go. Live blocks config has been generated. Great. So we have that. And now let's go ahead and let's create app room.tsx component. I'm going to go ahead and copy this. And here's a quick tip. You can see that it automatically, if you're following the documentation, it will automatically pre-fill your public API key. In case you are not following the documentation and you will just pause the screen and copy this, then I showed you where you can find your public API key. You go here in the API keys, right? And make sure that if you will copy this where your API key is already pre-filled, select the dev environment. So you can see that I'm here in the dev environment. I'm not using production for this. So just go ahead and copy this. And now we're going to go inside of source, app folder, documents, document ID, and create room.dsx inside. And just paste everything inside, just like this. There we go. So one thing we can immediately change now is this. We can add params from use params from next navigation. Oops. From next navigation like this. And then instead of using the ID my room, let's use params dot document ID. Make sure you don't accidentally misspell this. So document ID the same as you've written it here. I'm just going to give this as string type. So we get rid of the type script error. The key can stay like this for now. Don't worry. We are later going to change this into its proper environment variable. And we're not even going to be using the public key. We will be using a secret key. So don't worry about that. And now, if you try running your app, you will unfortunately run into an error. So let's go ahead and do npm run dev and npx convex dev. Now, if you're using bun and you install this with bun or pnpm or yarn, or if you used dash dash force, you will most likely not have any issues at all. But for us who use NPM and use dash dash legacy peer depths, we will most likely run into an error now. So I'm just going to go ahead and wait for my application here to load. I will refresh a few times and I will attempt to go into my application, into a project here. and looks like this time I'm not getting any errors. And that is because we actually don't get errors at this stage. My apologies. So yes, I did just a moment ago. I was recording this chapter and I run into errors, but I forgot where we run into them. So it's not here. This should work just fine. We run into the errors in the next part, which is when we add LiveBooks room to our page and actually enable LiveBlocks in our tip-tap editor. So let's do that. Let's go inside of page.tsx and let's wrap our editor inside of a room component like this. And let's import room from dot slash room like this. And I think that now if I refresh, still nothing happens. And then we will be adding this. use live blocks extension from at live blocks slash react tip tap. So let's go inside of the editor component. Let's import this. And let's add const live blocks. Use live blocks extension. And now we're going to add it here. So live blocks added inside of your extensions here like this. And this is where I ran into a problem. Now, again, if you are using PNPM, or if you're using BUN, you will most likely not see this problem at all. And now let's briefly talk about why this is happening. How come we are having this error? Well, remember, when installing our packages, what we used, let me try and expand this so we can go back to our npm install here. I scrolled too far up, I'm afraid. Let's see. Okay. There we go. So this is what we did just a moment ago, right? We installed all of these packages and we added dash dash legacy peer depths. So what does that mean? If you take a look at this package, live blocks react tip tap, you can see all the dependencies which they have in here. And then you can see all the peer dependencies which they have in here. Can you notice which one we are being warned about? We are being warned about a tip tap extension collaboration missing, right? So if you take a look back inside of the package JSON from the package we are, which is problematic, the LiveBlocks React tip tap, you can see that this one is in peer dependencies. So no, it's not LiveBlock's fault. It is our fault because we are using the dash dash legacy peer depth. So we are basically ignoring all of these things inside of here. Good thing is that these kinds of errors cannot silently fail. So if there is any kind of problem like this, it will very visually tell you that there is a problem, right? So it's not like your app is barely working and it will break at any moment. No, it will not even run if there is an issue like this. And the, well, the fix is quite easy, right? We just have to install whatever it tells us to install. So let's start by doing that. So module not found, tip tap extension collaboration. Let's go ahead and do that. I'm going to go ahead and I will just open a new terminal here, npm install, tip-tap extension, that collaboration, 2.10.2, dash-dash legacy peer depths. So let's add that. There we go. You can see that now we move to another package, extension collaboration cursor, which again is here in the peer dependencies, which we ignore with our legacy peer depths, right? So let's go ahead and add that. Let's also ensure the version here, 2.10.2 like this. And now we are missing this protocol right here, which is Y protocols. Now, I'm not exactly sure. I didn't really find the reason why we are missing this because it's not written here. And that package, which is now telling me that it's missing here, Y protocols, it's actually a peer dependency of this package, which is independences, right? I'm not entirely sure how deep does this dash dash legacy peer depths flag works. To be honest, this is the first time I've had to use it right for this React 19 release candidate. So I'm learning the same way you are now, but I guess it's failing to install properly. But like for us, all we have to do is just install this. And you can see this is my version at the time is 1.0.6. So npm install y protocols here. Let's do that. dash dash legacy peer depths like this. Let's install that. And let see if there anything else we have to do Looks like we don So looks like we finally resolved our issues And here the cool part I have now written something here And if I refresh now let's wait a second. There we go. I was scared for a second, but looks like it officially populated this inside of our room. So if you go inside of your live blocks, inside of your rooms, you will see that it has generated a new room now right here. And you can see it has the document size inside. And if you go inside of text editor here, I think you will be able to see the exact content inside. You can even see it in a JSON tip tap format. There we go. So it's official. We resolved it and it works. Let's take a quick peek at everything we've added. So the live blocks config is auto-generated with that npx command package lock as well package lock. You've seen us add the use LiveBlocks extension to the editor. You've seen me wrap my editor in a room and you've seen me create a room with my public key. And I've added the ID to be each document ID. That's why that's the name of this room here. So this is the same as my convex document ID. And in my package JSON, I have added LiveBlocks client, React, React TipTap and React UI. I have added extension collaboration and extension collaboration cursor because without those, it was simply failing. And I have also added Y protocols. So in one way, by having those errors, we now have even greater control of exact versions of our packages here. So perhaps you can look at that as a good thing. What's important is that it works. And if you happen to have any more issues, if it's telling you that it's still not working, what you can do is you can run this whole thing, which we got in the quick start here. Let me just show you. So text editor here, this entire NPM command, if you want to, you can go ahead and try and run it again and simply use dash dash force. It's not ideal, but I'm pretty sure that I've tried. And with this dash dash force, I had no issues at all, right? But force and legacy peer apps works differently. In my short exploration here, legacy peer apps might be a better option here because it can cause less problems in the future. So I think this is an okay solution for now. Nevertheless, you do know all of the versions that I am using. One thing that I will do since this React 19 stuff just came out, I will do my best to update this project's source code to as stable as possible, right? And then if you have access to the source code, if you're a pro member, you will be able to see all the changes I've done. And I will record some additional chapters for that on my platform. But great, we've got it to work. So what do we have to do next? We have added the room. We have added this. Let's see if we have any issues here in our console. As you can see, I believe that, first of all, we have a duplicate extension called image, so we can resolve that. And here is LiveBlocks telling us that the history extension is enabled. LiveBlocks extension provides its own. Let's go ahead and let's resolve that. So inside of our editor, we have to go to starter kit, run configure, and we have to turn the history off because LiveBlocks has its own history. So our undo and redo should still be working nicely. Let me try. There we go. It still works nicely, but now it uses the LiveBlocks version of that. So it's automatically collaborative here. Great. So that is working. And let's see what else do we have to do here. We added this. So we already have immediately render set to false. We did that in the beginning. So what I think we have to do now is the following. I want to add these to my layout.tsx. Let's go ahead and copy this. So basically, well, you will see now if you are not looking at the documentation. Let's go inside of source app folder layout.tsx like this. And okay, I will just add this. So React UI styles and React tip-tap styles. And I don't think nothing should really change now. But what I am going to do is the following. I'm going to copy my document URL here. And I have another browser open here. You can see that I'm in a completely different account here. So what you can do is you can, I'm using ARC. And in this ARC spaces, I can be logged in in different Gmail accounts, right? So either attempt to do that or go ahead and open a new browser, like use Chrome on one and use Safari on another or Mozilla and Chrome, right? Whatever you can do, or just use incognito mode to test out another account. And what I'm going to do is I'm going to copy the ID, the entire URL from the first document, and I'm going to attempt to join with my other login user here. And as you can see, here is the collaborative environment. So I can see that another user is right here. And I believe that if I start typing something here, another user sees that I am here. Great. So the collaboration has already started to work just fine. So if I go ahead and, for example, bold this and change the color, you can see that all of those things are happening in real time to the other user. One problem is that this user currently doesn't have a name. It's completely anonymous. So what we're going to do now is I actually want to wrap up this threads and the comments. I want to do that. And then after that, we're going to authenticate our users. Before we add these components here, I actually want to modify my globals.css just slightly. Let's go inside of appglobals.css and let's go all the way down. And what we're going to do is actually going to be quite similar to what they tell you to do inside of here. You can see that they give you some styles here, but we're not going to have this toolbar, right? It's going to be a little bit simpler here, but we can copy the floating threads and we can copy the anchored threads and we can copy this, the media max width like this. So we'll paste all of that here. So we've added floating threads. We have added anchored threads. And we have added this media query when to hide and show one another here. Great. So we have that working. So what we have to do now is we have to actually add the threads component. So I'm going to copy this entire thing here. and I will go inside of my documents document ID and I will create this under threads.tsx like this and I will paste it inside. So I'm getting used threads, anchored threads, floating composer, floating threads, all of that inside of here. This is my query to get the threads and I have added all of this class names here which we have defined in here. Let's check out, no, we can't really see this now. So we have to go inside of the editor. And let's add this right here, threads from dot slash threads. So just make sure that you have imported this and passing the editor to be editor like this. I think that should be it. So now let's try out and see if everything is still working here as intended. Looks like it is. We do have a slight weird loading indicator here. We will work on improving those loading indicators. But what I can notice is that no errors have appeared, which is great because we did add some new packages here in the threads. So I thought, well, maybe we were going to have to do some peer dependency resolving, but looks like we did not. So how do we test out the threads? Well, it's using this button right here. The problem is we did not enable that button to do anything because we didn't have the functionality to, right? But now that we have added live blocks to our extensions, we can revisit our toolbar component. And you can see I'm already left here in these. So basically we go inside of the toolbar and we have the sections, right? We have the first section and then we have the second section. And then we have the third section, which has a label comment icon of message square plus and on click to do comment. So now we have to do that. Let's go ahead and do the following. On click, we are going to do editor, chain, focus, add pending comment, run. And for this, we're going to do editor is active live blocks comment mark like this. So let me just refresh for good luck. And let's select something here. And let's click this. And there we go. I can now write a comment here. Let's try it out. And the comment will be visible right here. And the cool thing is, is that that is immediately available to the other user here as well. So they can also see those comments. They can also react to these comments. And they can also reply to these comments. The only issue is, is that all of these users right now are anonymously talking to one another. So what we have to do next is we have to find a way to authorize those users. But as far as I'm aware, we have resolved everything we have to do from this quick start. What's left is this to authenticate in our users. We can do that in the next chapter. And of course, we also have to properly name these documents because all of them now have their title as untitled document, which is not true. We also have to improve our loading states a bit. But the main parts of our apps are already getting all together here. Great, great job. we have to do next is implement live blocks authentication. Because right now, all of the users are anonymous. And even though they can interact with each other, no one really has any idea who these users are. And perhaps more importantly than that, this random user, which is not even in the same organization as I am, was able to access my document. So now we We will be fixing that. So let go ahead and do the following I going to go ahead and I going to go prepare inside of my dot environment dot local my live blocks secret key right here. Let's go ahead and immediately obtain that. So inside of your live blocks dashboard, go inside of API keys here and click on reveal key for the secret key, of course, and add it here. Again, I'm using my development project here. Great. Now, let's go ahead and also install a package, which I believe we don't yet have, which is liveblocks node, and it will be the same version as all of the other ones. So let's add npm install at live blocks slash node at 2.12.2 dash dash legacy dash dash peer dash dash. Oops, not dash dash like this. So live blocks node at 2.12.2. There we go. We now have node here. And now we're going to create an API endpoint which will authenticate our users through live blocks and clerk. So let's go inside of the app folder and let's create an API folder. Inside of here, using the same way of building client routes, we also build API routes that way. So we're going to create a folder called liveblocks-out, like this. And inside, instead of having a reserved page.tsx, we will have a route.ts. And that's how you create API endpoints in Next.js. Now let's export asynchronous function post, which will be the method we are using here. And let's prepare by importing a couple of things. We will need live blocks from live blocks node. We will need convex HTTP client from convex browser. We will need auth and current user from clerk Next.js server. let's establish convex here using new convex http client process.environment next public convex url and put an exclamation point at the end and double check in your .environment.local that you actually have that so next public convex url copy and paste it here so you didn't accidentally misspell one of the words here. Great. Now, instead of here, we're going to have a request, which is a type of request. And let's go ahead and let's use await out from clerk here to obtain our session claims in here. If we cannot obtain the session claims, we're going to return new response here, unauthorized, and a status of 401. Like this. Then let's go ahead and let's obtain the user using await current user. If we cannot find this user for any reason whatsoever, we will return back unauthorized as well. Now let's parse our room from request.json. So yes, we will have a room from await request JSON. How do I know that we will have a room here? Well, because it's where we are going to put this post request. We're going to use it inside of the room, inside of the live blocks provider. So once we give it an endpoint, it will automatically send the room to the body of that request. So that's how I know that the room will be inside. And now what we have to do is we have to find a way to fetch individual document from convex database. Because remember, we stored these documents inside of convex database, but the content is synchronized and stored in live blocks. But in here, we have some useful information. Which organization does this document belong to? What owner does this document belong to? So that's why we established convex here. And I believe that inside of our documents.ts, inside of the convex folder here where we have all these methods. We actually never implemented getById. So now let's go ahead and do that. Let's do export const getById, which will be a query. Let's add a handler here, an asynchronous context like this. There we go. So now basically what we have to do here is, first of all, we have to define that the argument ID will be one of the arguments. So ID, V.ID documents, like this specific documents ID, and we can then destructure that ID from the context here. And we will not be protecting this API route because all we're going to do here is we're just going to get the info whose document is this, right? We are not actually holding any sensitive data here. So throw new convex error here, document not found, and otherwise return the document. There we go. So a very simple get by ID route. And now we can freely use it here in this post endpoint. So let's go ahead and do that. After we obtain the room, that will also be, so that's basically the room ID. So that's our ID. So const document is await convex query API, which you can import from, well, the convex folder. You just have to find your way to it, right? So all the way out here into this folder like this. So that will be API.documents.getById and passing the ID to be room like this. In case there is no document, we can return a new response unauthorized here as well, even though this should technically never happen because we do that check here. So yeah, I suppose maybe we don't even need to throw an error. I guess we can just do this then. If we're going to handle error throwing here, and this is the only place where we use it. So yeah, how about we do that instead? We are going to keep this method like this, and we will only access it in here. If there's no documents, we're going to assume that we are unauthorized to see that document. And now what we can do here is const is owner by checking if document owner ID is equal to user ID and const is organization member to be if document dot organization ID is equal to session claims question mark dot organization underscore ID. Or can we do this? Yes, session claims will exist like this. And then we're going to do if we are neither an owner of this document and we are not even an organization member. In that case, we can also throw unauthorized. So only those people who are authorized to see this will be able to interactively collaborate and see the content of that document. So now let's define live blocks. And I actually want to define it here. So const live blocks will be new live blocks, which we have imported from node above. And let's pass in the secret to be process.environment. And you can just copy live blocks secret key. Make sure you don't accidentally misspell it. Put an exclamation point at the end to fix the issue. There we go. So now let's do the following. Const session will be live blocks.prepare session user ID. and we are going to pass in the user info. Name will be either user full name from clerk or anonymous and avatar will be user.image URL. Like this. And then we're going to give this session permissions. Session allow for this room. Session full access. And then we can destructure the body and the status from await session.authorize and we can return new response body and the status. So every user that attempts to visit any room will go through this process. We will first attempt to find the document assigned to this room. If there's no document to be found, we're not allowing any connection to be made. If the document was found, but we are not an owner and we are not even an organization member, we will throw back an error. Otherwise, we will allow that user to obtain a session and to become a member of that collaboration environment. So now what we have to do is we have to actually use this inside of our room. Let's go ahead and do that. So remember, our endpoint is now localhost slash API slash liveblocks dash out. So let's go inside of documents, document ID, room.tsx right here. And we will no longer be using the public key because we now use the secret key on the backend, right? So this is what we're going to do. We're going to add auth endpoint here, slash API, slash live blocks, dash out. Let's also add throttle here to be 16. I think this will simply increase, visually increase the performance. Like it will be synchronized faster. Great. So I think this is enough for us to actually try out whether this is working or not. So I'm going to refresh on a room and a document that I own. And we're going to see what happens. As you can see, it seems to be loading just fine. There we go. Post request to API Live Blocks and it returned 200 because I am permitted to see this room because I am on my first account where I actually created all of this, right? This resume 123, I think that's the room. You can see that I created this room. But let's now try this random user, which I used, and I just copied the URL. If I refresh now, let's see what will happen. Looks like this user can access it as well. So we seem to be having an issue here, and it's good that I tried it because I was almost certain that we will not be able to do it. So now it's time to debug. Why exactly is this user able to do that? I'm going to go back instead of live blocks out because the whole point of this is that that user cannot do that, right? So let's try and do some things. I going to go ahead and check out console log is owner and is organization here Perhaps I doing some invalid logic block here Let me try and refresh this like this Oh, I think that, oh, is organization member is true. That's interesting because I'm not an organization member here. Document organization ID, session claims organization ID. So I'm going to go ahead and debug this a little bit, why this is happening. Okay, I think I understand why this is happening. And it's quite an interesting issue. So I console logged session claims because this is what we use to check if we are an organization member, right? And it's very curious that I receive, is organization member set to true? But my session claims doesn't have the organization ID. And I was wondering how is that possible until I realized that this document here is in my personal account, which means that it does not have an organization ID, which means that we are comparing undefined with undefined. So that is the issue. So we have to check if document organization ID exists in the first place like this. uh let's try it now so i will refresh on myself here this should work and let's try this now so i will just add some spaces here because i'm very interested in what will happen now and there we go now i'm getting errors okay there we go is organization member is now undefined Let's actually make this work properly so we can do this. We can wrap this entire thing and put two exclamation points here. And now if I refresh here, you can see that it will say explicitly, is organization member false and is owner false, right? So we were comparing undefined with undefined, right? That was the issue. Great. So, okay, that is an interesting thing that I completely missed. And now I'm worrying if I miss that anywhere else. How about we do a quick check inside of our convex here, inside of documents.ds. I believe I did a lot of these checks. Yes. So, is organization member, we have to do the double check everywhere. So, first check if document even has the organization ID and then compare that organization ID. If you can think of a faster syntax to do this, feel free to do it. So I will just copy that here for the update ID, and I will copy it here for the remove by ID. So the reason we didn't encounter this before is because our front-end UI does not allow someone else to even see the option to remove or delete something, and that they're not a part of organization, right? So that's why we didn't encounter this. I think it's just those two places. So now what I want to do is I want to handle this error, which happens in the other user, more gracefully. Because right now I can see it's getting these errors. It's unauthorized, but it's not exactly a nice user experience. Because this can happen if you get kicked out of an organization, for example. So it's not just malicious users who will try to do this. So we need to handle these types of stuff gracefully. So what I suggest we do is we create a global error boundary with a reserved file name error.tsx like this. So now we have this. We have to mark this as use client because the error boundaries work as a client component. Let's simply do an export default error page. And inside of here, let's return a div with a class name. minimum height of screen flex flex column items center justify center and space y6 now let's add a div with a class name text center and space y4 inside another div with a class name flex and justify center. And then let's create a class name, background rows 100, padding of three, and rounded full. And inside, we're going to render alert triangle icon from Lucid React. And let's give it a class name, size of 10, and text rows 600. So now if you stumble upon an error, which this should give me. Okay, so it looks like even though I got an authorized 401, I was hoping to get redirected to that error page. Yes, so okay, I was hoping we will see the error page now. So here's what you can do. I think you can just go inside of your layout here. So go inside of your app folder layout.tsx and just throw an error. So throw new error. Whoops. Okay, maybe not in the root layout. Let's try throwing it inside of documents, document ID page. How about we try throwing the error here? There we go. Okay, now we can see the error page. So for now, just inside of your document ID page, throw an error so that you can see what you're developing because otherwise we're just blindly developing the error screen and then later we're going to see how we can redirect the user to this screen once they've been told they're unauthorized okay so what we're going to do now is go here add a new div add a class name space y2 we're going to add an h2 element something went wrong like this. We're going to give this a class name of text extra large, font semi bold and text gray 900. We're going to add a paragraph error dot message and we can extract the error from the props here. So we have error and we have reset. So this automatically comes from the error page. error is a type of error and it can also have a optional digest which is a string and the reset is a method like this so render the error.message here and then outside of this div open a new one with a class name flex item center and gap x of three and inside render two buttons from components ui button the first one here will have an on click reset and class name font medium and px of six and it will render try again text then we're going to have another button here which will say go back it will not have an on click it will just have font medium it will have a variant ghost it will have as child prop because inside we're going to be using a next link element with an href to go to the roots page so just make sure you've imported next link and now there we go this is our error page and it's showing oops, because that's what we threw in an error. So if I changed my error here and refresh, that's the error it shows. Try again, we'll try again and go back. We'll redirect you to this root page. Great. So we can now remove the error from the document ID page right here. Great. So we have just added the live blocks authentication and let's see what the difference is now. So here's what I'm going to do. We can resolve the error of us not being allowed in a certain document in a moment. I mean, the error is happening. We are just not having a pretty UI for the user to see. But what's important is this. I'm now going to go ahead, create a new organization, call it test.org. I will switch to that organization and I will invite this user here. So tutorial mailing. Let me send the invitation to that user here. Because I now want to see their usernames. There we go. So I can join this organization. Let me go inside of this organization. I'm going to create a new document here. I will go inside of here. And they should have all permissions to work inside. That's great. And now this other user already sees that blank document. I just refresh for good luck. So they are in the same test organization now, right? And let's see, I'm going to be writing something here. Can this other user finally see this user's name? And there we go. So you can see they are no longer unauthorized, but it's actually fetching their names from their Google logins, which we used with Clark. Amazing, amazing job. What you can also do now, actually, one thing that you can't do now, that you still can't do is tagging the other user. Because right now, for example, if I attempt to write a comment, hello. it's still anonymous, right? So there is one more thing we have to do. So we're going to do that in the next chapter. And then we're also going to add some avatars here to show you who is currently active in this room. And we will also implement a notification system so you can see if someone tags you because the LiveBlocks has a tagging feature, but it's currently not enabled. Great, great job. now let's go ahead and let's implement proper user mapping here so in the previous chapter we did enable collaboration in a sense that we saw two different cursors typing here but what we weren't able to do is properly see our names when we are writing something so we are definitely authorized to see this document. And we did share the user info. If you're remembering the live blogs out here, we do share our user info, but for some reason, none of that is visible at the moment. So let's go step by step where we left off and let's develop this. So I'm just going to remove this console log because I no longer need that. And also let's change this client side suspense in our Room component to use\nUse the full screen loader. And let's give it a label room loading like this. So now when you visit this page, you have outloading and then you have room loading. I just don't like how the full screen loader wasn't exactly full screen. So let me just wait a second. What we have to do is we have to change the structure here. Instead of page, the room should encapsulate this entire page, not just the editor, right? So when it comes to loading, I don't want anything to be visible besides my room. There we go. So now the room is encapsulating the entire thing, including the editor. And now when you refresh, you have outloading and then you have room loading. And then you are inside. And if you're wondering, I purposely left this. So remember how I told you that we were not seeing this error screen when we failed to authenticate. So right now I am in this other user and I'm attempting to see the other user document. You can see that I'm getting these errors, but I thought that I, for some reason, I will never see the error screen. Well, the thing is LiveBlocks does the attempt multiple times. So I think after it fails for the fifth time, that's when it will actually throw an error and it will fall back to our error boundary. So I think we actually don't have to do anything here. We can just let it stay like this. And after a certain time has passed, it will show the error screen. And I think this is quite useful because there we go. You can see that now it works after five attempts. Because, for example, maybe we weren't able to load access because, I don't know, clerk was down or convex was down or live blocks was down, right? So it's good that we do five attempts. You can see that it takes quite a lot of time for it to completely give up. But once it does, you're almost certain that, yeah, it was definitely something wrong with your access here. Great, so we have resolved that. Now I will focus working on this where I have all the proper access. And what we're going to do is we're going to go back inside of room here. And now we're going to go ahead and develop a way to fetch our users. So for that, we're going to add a new prop to the LiveBlocks provider, which we will call Resolve Users like this. And for now, let's just return an empty array. And we will also need Resolve Mention Suggestions, which we can also return an empty array. And the last thing we are going to need is Resolve Rooms Info like this. And we can also return empty array here. So we need to populate all of these methods here. Let's start by resolving the users. So I'm going to go ahead and I'm going to add const users set users. And I'm going to call useState here. And I will add an empty array initially. Let's import useState from React here. What I will do now is I will define a type for the user. So type user here will have an ID of string, name of string. Oh, I think, yeah, it will always be a string. I think actually it will be string. Yeah, okay. Let's make it always be a string. And avatar a string. The reason I was so confused about this is because if you remember in my live blocks out, name can be undefined, right? So that's why I wasn't sure, okay, will I turn it into anonymous in here or will I do it somewhere else? But let's leave it like this for now. So now we have a type user. So we can set this type to be here. So user and an array of those like that. And now what we have to do is we have to actually implement the function to fetch this user. So, instead of document ID, I'm just going to create a server action. So, actions.ts, like this. Not tsx.ts, inside of my document ID. I will mark this as use server. I will import out and clerk client from at clerk next.js server. And then I'm going to export asynchronous function called get users like this. I will obtain the session claims here from await out. I will get clerk from await clerk client. And then I will get the response from await clerk users get user list for this specific organization ID using session claims question mark dot organization underscore ID as a string like this. and then let's do const users to be response.data.map get the individual user and return an object id user.id name will be user.name user.fullname or it will be user primary email address dot email address like this so let me see can then okay name can still be undefined then uh okay or that or anonymous like this so we have all the fallbacks or you can remove this one if you don't want it because yeah full name doesn't exist if you if your user does not use google login if they use just email login they will not have full name so we can already fall back to anonymous here or you can attempt to get, you know, user primary email address dot email address. And then if even that fails, it's anonymous. And avatar can be user image URL. And return the users. And I actually want to do the same thing in my live blocks, API live blocks out here. So let's do that here. If possible to get the email address, let's do it. Otherwise, we fall back to anonymous. So if both of these things are not available, for some reason, we just can't fetch this user's name. And we're going to call them anonymous in that case. Okay. So now that we have this ready, the get users method, we can go back inside of our room. And what we can do now is the following. We can implement const patch users. Let's add use memo here from react like this. and let's go ahead and define an asynchronous method here and let's open try here const users let's actually do const list to be await get users like this so you import get users from dot slash actions because we have defined actions in the same folder here in the document id so get users and set users will be that list of users like that and in the catch here we can do toast from sonor for example toast.error failed to fetch users and go ahead and add an empty dependency array here like this and then in the use effect here we're simply going to call fetch users. Patch users in here. And now that I look at it, maybe we don't even need this to be memoized because what we can do is we can just do this and then this won't necessarily have to be a part of the dependency array. But if you want this to be a part of the dependency array, you need to memoize it. So, okay, let me move that here. like this. And now we have the users. And now that we actually have our users, we can go inside of our resolve users method here. And in here, we can destructure the user IDs, which we have loaded for this room. And then what we can do is we can return user IDs.map like this. We can get the user ID here. And from our users, we can find in the individual user if user ID is equal to this user ID. Like this. Or we can fall back to undefined here just in case. And now let's do the resolve mention suggestions. So for here, we also have to implement search via the text prop here. So let's go ahead and do the following. let filtered users will be our users. And then if we have text, meaning we are searching for a specific user, we're going to do filtered users will become users filter, get the user. And then we're going to do user.name to lowercase includes text to lowercase. though i think that we don't have to add a question mark here because we've made name required right there will always be a way uh to call a user something even if that is anonymous and then let's do return filtered users dot map get the user user id and i think that this already should be enough for us uh to try and mention someone let's see so i'm loading here and there we go. It already loaded these users. And now if I try here, there we go. You can see that I can now tag those other users which are in this room. So whoever was in this room at least once will become a part of this user IDs here. And if more users come, then this will refetch all of those new users which just came here. So this action will be called again. And basically, we don't have to worry about, um, because every time, uh, sorry, what I want to say is that the only people who can visit this are organization people, right? So that's why we don't have to worry about constantly refetching or something. Uh, because if we do invite someone or something, you know, that is okay cause for a full refresh right So those kinds of things are okay to be out of sync because they will be in sync uh very shortly after maybe one refresh or something But most of the time this will work absolutely perfectly Great So this seems to be working as intended So now what I want to do is I want to go ahead and I want to add little avatars so we can see exactly who is in this room. let's go ahead and let's go inside of our documents document id here and let's create a new file avatars.tsx let's define a constant avatar size to be 36 like this and now let's go ahead and Let's define one avatar. So, const avatar. Let's create an interface, avatar props. It will have a source, which is a string, and a name, which is a string. Let's go ahead and assign the avatar props here. And let's destructure the source and the name. Inside of here, we are going to style the avatar. Let's give this a class name. group minus ml2 plex shrink zero place content center relative border 4 border white rounded pool and background gray 400 besides the class name let's use the style prop to add the constant values. So width will be avatar size and height will be avatar size. The reason I'm separating those in a constant is because we are going to have more components here in a moment. So it's safer to put it in style than to potentially break tail with their just-in-time compiler rules. Inside another div, which will render the name, and this div will have opacity 0 by default. But when we hover over a group, which we're later going to have, we're going to put the opacity to 100. It will have absolute value, top, full, py1, px2, text white, text extra small, rounded large, margin top of 2.5, Z index of 10, background color of black, white space of no wrap, and transition opacity. And then we're going to have an image here with an alt of name, a source of source, class name of full width. Actually, we can use size full and rounded full. And that's it. That is our avatar component. Now that we have the avatar component, we need to implement the avatar stack component. So const avatar stack will get the users using useOthers, not from live blocks react, but from live blocks react suspense. Like this. While we are here, let's also add useSelf inside. inside. And let's also prepare client side suspense from, oh, I think actually from React right here. And let's mark this as use client like that. Inside of this outer stack, we get the others, we get the current user using use self. And if there are no other users here, we are not going to display ourselves only right so we're just going to hide this entire avatar stack otherwise let's go ahead and let's return a fragment and a div which will hold our avatars with flex and item center first we're going to render ourselves so current user and then we're now display a div with a class name of relative and ml of two and an avatar here. The avatar will have a source of current user info avatar. It's not currently typed. We will fix this in a moment and the name will be you. We will fix this in a moment. This is because we have to add this inside of the live blocks config here. You can see that inside of the info, we did not add the name and the avatar. Actually, we can do that right now. So go inside of your live blocks.config.ts. It should be in the root of your application. So not inside of the source folder, it should be here. But, you know, wherever it is, find the file, find the user meta, and for example, enable the name and the avatar and save. And the moment you save this, you can see that now we can see the avatar here. And we know that this will be an avatar because of our live blog's alt file. Because in here, we define the user info with name and avatar. So make sure you didn't accidentally misspell these things here. Great. So we now have this. And now what we have to do is we have to render other people's avatars. So let's add a div with a class name here of flex. And let's do users.map. Destructure the connection ID and info from each of these users. And then return an avatar for each of the users. Give it a key of connection ID. Give it a source of info avatar. And give it a name of info.name, like this. And then what we're going to do is add a separator here from components UI separator. like this. And now, one more thing which we are going to do is finally export const avatars. And avatars will do the following. Oops, let me just fix this. It will return client-side suspense and then render the avatar stack. And fallback will be null. Basically, what this means is that all of these methods, use others and use self, they cause a suspense to be triggered, which means that inside of our room, this full screen loader will be showing. And we don't really need that, right? So we don't really need the entire room loader to still be showing while we are just loading the avatars. So that's why we are wrapping it in its own suspense. So then regardless if the avatars are still loading, we can already start typing in our document. So that's why we are doing that. Great. What we have to do now is we have to render the avatars here. So let's go ahead and do that. We can do that inside of documents, individual document ID and find the nav bar here. And now let's just go ahead and import the avatar. avatars from dot slash avatars like this and let's go all the way down where we have our organization switcher and user button here and let's simply add avatars like this so right now as you can see no one besides me is in the room here but if i go inside of this other profile here in the test organization. There we go. You can see how I have two people here, but I've messed up my separator. So let's go back inside of the avatars here and let's properly add the separator here. Let's give it an orientation. I think it should be a horizontal. My apologies, vertical. And give it a class name of height of six. So now we can see how in here we have all the users who are in the room and then we have a separator for our usual options here. Great. So now we can always see who is in the room. So if I leave here, you can see that my other user here now will not see anything because it makes no sense to just display our icon because we can already see our icon here. So it's just going to be confusing. And that's why we also put the separator inside of there so that we can render it depending on if we even render that component or not. Great. So we just populated our users. We populated our mentions. So yeah, we can now go ahead and mention other users here. Hello, for example, we can do all of those things. What's left is in LiveBlock's side is to add an inbox system so users can individually see the notifications that they're tagged somewhere. And also we need to enable the margins because they're still not doing anything. Nevertheless, great, great job. Now let's go ahead and let's implement the inbox functionality. So we're going to go ahead and do a similar thing as we did with the avatar. So let's go inside of app, documents, document ID, and create an inbox.tsx inside. Let's go ahead and mark this as a use client. Let's go ahead and let's import client-side suspense here. And let's export const inbox here, which will only be responsible for rendering our inbox inside of a client-side suspense. And down here, we're going to develop inbox menu component. and then we will render the inbox menu inside of here like this and the fallback can be no for now now let's go ahead and actually implement the inbox so for this we're going to have to import a couple of things here let's import bell from lucid react let's be more specific let's call it bell icon and let's import inbox notification from live blocks react ui and inbox notification list from live blocks react ui and let's also import use inbox notifications from live blocks react so all of those things now let's go ahead and let's do the following inside of the inbox menu we going to get the inbox notifications from use inbox notifications hook Now we need to add imports for our dropdown menu. We've already used this component a couple of times before, like this. So let's go ahead and use the dropdown menu here. let's add drop down menu trigger let's give it an as child prop let's use a button component the button will have a variant of ghost it will have a class name of relative and it will have a size of icon inside of a button we're going to have a bell icon with a class name of size 5 and we're going to have an inbox notifications.length check. If it's more than zero, then we're going to render an indicator for those inbox notifications. Let me just check. So if inbox notifications.length, inbox notifications is possibly null, that's interesting. So it can be undefined. Okay, then we'll just use this. Inbox notifications.length, okay. So let's see what is the issue here. I think it matters where we import use inbox notifications. Yeah, because if we use it from slash react, then it can be undefined. But if we use it from slash suspense, then we no longer have that issue. Great. So let's just confirm that we're using live blocks react suspense for the hook here. Great. So now that we have the inbox notifications, we can go ahead and add a span, which will be a little badge, which will basically tell you how many notifications you have. And let's give this a class name of absolute, minus stop, minus one, minus right one, size four, rounded pool, background sky 500, text extra small, text white, flex items center, and justify center. Great. So that's the bell icon. and now let's go ahead and create our drop down menu content which will have an align of end and a class name with auto and inside we're going to go over our inbox notifications that length and again confirm if we have at least one inbox notification and then we're going to render the inbox notification list like this. Otherwise, we'll render a div which will say no notifications and give this a class name of padding 2 with 400 pixels, text off center, text small, and text muted foreground like this. And now let's go ahead and actually render the inbox. So I'm going to go inside of my navbar here. and before the others rendered the inbox from dot slash inbox. So let me just go ahead and see if I've imported this correctly right here. There we go. And now instead of a room, you should be seeing an inbox. I'm not exactly sure why we're not seeing one at this time. It could be because it's loading. I'm not sure. Let's see. inbox menu return drop down menu drop down menu trigger so yes this fallback here would perhaps fall back to null let's see if i add p loading here will that render yes it seems to be stuck at loading so let's see why that is happening here uh looks like it responded with 500 internal server error. Oh, yes. I think I remember having this issue when I first started developing. Yes, you can see that all of a sudden our app is failing to authenticate. So we have to do one thing differently. We have to go back inside of our room component right here because notifications can be used outside of specific rooms. But inside of here, our endpoint is made so that it works only for a specific room, right? Our live blocks out needs to work by having a room passed, right? So right now, this is not passing a room. So this is what we're going to do. We already have the params in this room component. So we're going to modify this because this doesn't have to be a string. It can also be an asynchronous arrow function. Let's get the endpoint to be slash API live blocks out like this let's make the room be params document id as string make sure you don't misspell this so double check that it's the same one you're using here and then we're going to do const response to be a way to fetch endpoint method post body json stringify and pass the room so now we ensure that we always have the room because we explicitly do that. And let's return a wait response JSON like this. So now there we go. I can load the room again. And there we go. You can see that we now have our notifications here. So let's go back inside of the inbox and let's bring this back to null. So we don't have this ugly loading status. And let's continue developing the inbox. So inside of the inbox list, we're going to iterate over our inbox notifications.map, get individual inbox notification. And inside of here, we're going to render that inside of the inbox notification component. Let's give it a key of inbox notification.id and inbox notification will be inbox notification. So now when you click here, There we go. You can see that someone mentioned me in this room. I believe I mentioned myself here. So that's why I get a notification here. And I believe that my other user, who is in another room here, will have even more notifications here. There we go, two notifications, as you can see. So you can see I even have the threads here. So yes, this user has more notifications. But we have an issue. We are not loading the names of our rooms. And that is because in our room.tsx, we did not do this. We did not resolve the rooms info. So now let's go ahead and let's do that. So in order to do that, we have to go back inside of our convex documents. let's go at the top of our app here and let's export const get by IDs. Let's call this a query arguments IDs, which will be an array of document IDs. Let's add a handler. Let's destructure the IDs from the arguments here. And what we're going to do is we're going to prepare the documents array for const id of ids const document will be await context database get id if we do have a document we are going to do documents.push id document underscore id with the name document.title because in our schema we have defined the title to be the name of the document right Else, if the document doesn't exist, that could mean it was removed. So we are going to keep the ID here to be the current ID, right? Which we are iterating over. And the name will be removed document. Or we can just call it removed or deleted. Whatever you want to call it. You can maybe give it a special sign like this. And this will be documents like this. And finally, return documents. There we go. So we have developed our get by IDs method. And now we have to go back inside of source app folder documents, document ID actions. And inside of here, import convex HTTP client from convex browser. And we can copy the same thing we had in our route here. So basically, we need to initialize convex here like this inside of our actions now. And now let's just develop the method we need. Export asynchronous function get documents. Like this, IDs, individual ID from generated data model. And this will be a type of documents and an array of those. Return await convex query API documents get by IDs. like this. And we also have to import the API. There we go. So now we have our getDocuments method. And we can now go back inside of a room. So let's go inside of our room component, which is inside of our documents, documentID, room.tsx. And now let's import that from the actions. So that will be get documents here like that. And let's see. Where was I? Okay, resolve rooms info. So basically what we have to do here is we have to make this an asynchronous function. We have to destructure the room IDs, which are needed. And inside of here, we're going to do const documents. Oh, wait. Get documents. Pass in room IDs as ID. documents and array. Make sure you have imported this ID type. I did it automatically. There we go. So ID from convex underscore generated data model. The same thing we had to do previously here. Once you have that, once you have the documents, go ahead and return documents.map, get the document and return an object id and name document dot name Because inside of our getDocuments let take a look at our PlumDux function we already only push name and id. So that's why this route isn't protected, because it's only giving the most basic information here. So now, if we try and refresh, I think that this should already be working just fine. There we go. In blank document, in blank document. And what you can do here is actually a little issue, as you can see, because once I click here, I should be seeing a menu, but I'm not seeing it. And it's basically a conflict with Z indexes. And you can fix it by going into the source app inside of your globals.css. and let's go all the way down here. And basically what we have to do is we have to target a very specific div. So div, open square brackets, data, radix, popper, content, wrapper, z index 50, and make sure to add important here. And now if you click more, there we go, you can see the option to delete modification. You can also mark them as read and you can delete them completely. There we go. So what I want to do now is kind of fix this flashy way the bell appears. And we can do that by going back inside of the inbox right here. And we're just going to copy this button, this entire thing, right? And let's use that as the fallback like this. but we're just going to modify it doesn't need to have a span it can just be an empty bell icon like this so this is my fallback a button encapsulating the bell icon so now when i refresh you can see that it appears normal right but you don't even know if it's loaded or not so what we can do is we can make it disabled in this state so it's not even clickable right so now you will kind of be able to notice here you can see how it was disabled for a second and that was basically the loading state just one thing I want to add here is another separator so let's just go ahead and do that so in my inbox menu I can do that I can go ahead and put this in parenthesis in fragment and then I can add a separator from components UI separator, and I can give it an orientation vertical, and I can give it a class name of height of 6, like this. There we go. And there we go. We now have two very nice separators in our navbar. Oh, yeah, but the issue is that it will dynamically show and hide. So I guess that we can do the same thing here. in our fallback menu. Copy the separator here. Let me just see. Did I do this incorrectly? I did not. Okay. So after the button here, we're going to add it. And let me just indent all of that here. So now even in the loading mode, there we go. It's visible. Great. And let me just check inside of my avatars that I have the same one. Yes, it is the same one. Great. So we now have this developed. And now what we ought to do is well implement these margins right here. Great, great job. Now let's go ahead and let's implement our margins. So first of all, what I want to do is I want to go inside of my liveblocks.config.ts. And inside of here, I'm going to find the storage. And inside, I'm going to add left margin to be a number. And then I'm going to add a right margin to be a number as well. Now, we ought to go to room.dsx. And you can see how already we are having some issues here. That's because we have to provide the initial storage here. The initial storage will have left margin set to 56 and the right margin set to 56 as well. I'm going to collapse my props here so we can see them in a more readable way. And now what we have to do is we have to go inside of our editor component right here. And now we have to fetch from our live blocks store. So in here, instead of it manually being written 56, we will be using the store. In order to do that, we need to import use storage. So let's go ahead and add that. I'm going to import use storage from live blocks react. Let's see if we can import from suspense as well. I think it's better to do from suspense. I'm not sure. Let's try first without suspense like this. Use storage. And then I'm going to define left margin here to be use storage, get root, root left margin, right margin, use storage, root, root, right margin. So we have left and right margin set here. So now let's go inside of our editor here. Let's change this from normal quotes to, oops. So let's change this from normal quotes to be backticks. And then let's change this to be left margin. And we can also fall back to 56 in case these cannot load for any reason whatsoever. And now in here, let's replace this with right margin. so just make sure to fall back to 56 right so right now nothing should really change the margin should still be exactly the same and your app should still be working just fine and now finally let's go to our ruler component ruler.tsx and now we have to import use storage again so let's import useStorage from liveBlocks.react and let's also import useMutation from here as well. So now instead of having state for this, we're going to replace it. So const leftMargin will come from useStorage, which will give me a root, rootLeftMargin. And then I'm going to have a const and we can call it setLeftMargin here, which will be useMutation, destructure the storage, get the position which is a type of number and call storage.setLeftMargin to the new position like this. And then you can remove this and make sure, let's see, something is missing inside of my use mutation here. And that is the dependency array, which is here at the end. So that's what we need. So right now, let's see. So left margin is possibly null. So what I want to do is I want to pull back this to 56 here. so we get rid of those errors. And let's confirm that our set left margin can still be used. So I no longer need to do this. I can remove the to-do from here, I believe. So let's go ahead and try it out. I think that now we should be able to programmatically move our left margin. And we can. And what's cool is that it's immediately reflected, there we go, to another user. So I'm between two different accounts here. And you can see that in real time, you can't even notice how well it works. Great. So now we have to do the same thing, but for the right margin. So let's go ahead and we can copy all of this. Let's call this right margin. Let's call this set right margin. And let's remove this. And it should work exactly the same. so i'm going to go ahead and refresh this and now we should be able uh oh looks like i am doing something wrong here yes i forgot to call right margin here there we go let's refresh again and right now i believe everything should work just fine there we go we can now control our margins from here and it is immediately reflected to all other users that are watching this channel and it's also saved, right? If I refresh, it will remember my margins. Great, so we finally brought these margins to life. Perfect. I believe this brings us to an end of Live Blocks integration. What we're gonna play around with next is enabling us to rename the document from here, giving us some saving indicators here, enabling these two buttons and of course searching through our code for anything left for to do. For example, we still have to add the template click initial content. That will be quite easy to do. Don't worry. And we also have to use document names here. So just a couple of more things we have to do before we are able to deploy the project. Great, great job. Now let's go ahead and let's enable us to rename the document from here. And let's also fix this few issues which we have. So in order to do that, we're going to have to fetch the document from our database whenever we visit this page. Because right now, the only fetching that is done inside of documents document ID is actually the room, right? The live blocks room. Nothing else is being fetched. We actually have no idea what document this is. And you already know how to fetch documents, right? We do that in our homepage. For example, here we call use paginated query. So we could very easily just add use query in our document ID page. But in Convex, they've recently added a way...\nto preload data using a server component, which basically means that it's way faster than usually through pure client side. So let's go ahead and do that. This is how we will do it. I'm going to go inside of documents, document ID, and I will copy everything inside of this page.tsx. And I will create a new file inside called document.tsx. So we will treat this document.dsx as a component. And inside, I'm just going to paste the entire thing. So now we have to slightly modify this because it's not going to be called a document ID page. It will just be called document. So this will be document props. And this will be a document component here. And it will not have a default export. It will have a named export like this. And we can also modify the props. It will accept a preloaded document here. And we're going to import preloaded from convex react. And then we're going to give it a type of API from convex generated API. The documents and then we're going to have a get by ID here. We already have implemented this method get by ID. And we have used it in the live blocks out, I believe. Let's just see live blocks out. convex. Yes, we have already used it here. So we know that works. And then inside of here, we're going to have reloaded document. And then we can just remove our params here because we will not be using it, but everything else should be just fine. So we can leave it like this for now. And now let's go inside of our page.tsx. And since we just copied everything from this inside of document, what we can do here is remove everything and start from scratch. So let's call this document ID page like this. See, we didn't have to remove everything, but it's okay. We can write it again. Interface document ID page props, params, a promise of document ID, which will be an ID of documents like this. So now we have the document ID page props once again. Let's have the params here. We can turn this into an asynchronous component and we can destructure the document ID from await params like this. And inside of here, what we will do is we will just returned a document from dot slash document like this. So this will be a server component in which we are going to preload the document. And then instead of here, we will be able to use it. Yeah. A few versions ago, you were not able to do that with convex, but now you can, which is great because one of the big features of Next.js are server components and how fast they are. So let's go ahead and do the following. we have to import out from clerk next js server we have to get get token from await out and we have to define the token using get token and we have to pass in the template here to be convex otherwise it's going to be undefined like this and now what we have to do is we have to import preload query from convex. So import preload query from convex next JS. You can see that they have specifically made this for that. So now we have the token here, which we have to extract like this for server components. And then we're going to do, if there is no token, we can immediately throw an error here. Unauthorized. And then let's preload the document. So preloaded document will be await preload query. In the first argument, we're going to pass our API endpoint. So make sure you import the API here. API.documents.getById. In the second one, we're going to pass in the actual document ID, which we extract from the params here. And then we also have to pass in the token here. like this. And let me just see, did I do this correctly or not? I think this needs to be awaited. Like this. There we go. So now inside of our get by ID, we also have the token if we want to use it, right? So I just wanted to show you how you would authorize your user with a preloaded query, Because if you were just to call this, it will not have this, for example. You would not be able to get the user without get user identity. Because most of this are called in a client component where we attach the token through use query. But with freeloaded query, we don't do that in server components. So I just showed you how you can do that with clerk and convex. But in our case, get my ID is not even protected because we use it in live blocks out and we don't get any sensitive data from here. So now let's go ahead and pass in the preloaded document in here with preloaded document. There we go. So now what I want to add is inside of the document ID here. Let me just close everything else. Instead of document ID, I want to add a loading.tsx. So this will trigger every time a suspense is being triggered. And in our page here, while we await here, while we await here and here and here, all of that can be handled in this loading file right here, which is like a boundary for this document ID because it's inside of that folder. So let's go ahead and create this loading.tsx, which will be very simple. So loading page, and we are just going to return full screen loader. And let's give it a label document loading like this. So now if I refresh this entire page, you see outloading, room loading. And I think briefly there was document loading, but it could be that it's just not fast enough for us to see. So inside of page here, what we can do is, for example, we can add a console log loading just so we know that it's happening. Let's see, or maybe we forgot to do something here. There we go. Loading, loading. Great. That's what we wanted. As long as that's happening, we don't care about anything else. Great. So now we have the document ID page, and in the document, we now have the preloaded document. So what we have to do here is we have to define the document with instead of use query in this client component, which we also have to mark as use client explicitly. We have to add use preloaded query from convex react. So make sure you've imported this. And inside of here, simply add the preloaded document. So why did we have to do this if we already have the document here? Well, that's because remember, the document can still be updated either by us or by some other user. It can be renamed, for example. And we want to keep that convex reactivity in real time updates. So that's why we have to pass it in this query. So the initial load is done with a server component. And then we will continue updating that query through a client component with use preloaded query. And now that we have the document, we can, for example, pass it to the navbar. So data and passing the document. And now let's go ahead inside of the navbar component and let's give it some props here. So interface navbar props will accept the data, which is a type of document, which you can import from convex generated data model. So a type of documents here like this. Let's go ahead and assign the navbar props. Let's destructure the data here like that. And now that we have the data, we can already improve some things here. For example, in onSaveJSON, instead of document.json, make sure you use back text here. And you can add data.title.json. And we can remove this to do, for example. And I will just copy this here like this. Now do the same thing in onSaveHTML. So data.title.html, and I can remove the to do here. and on save text, same thing. And I can remove the to do here. So for example, that's one thing which we can immediately write off. What we have to do next is we have to pass this to our document input. So let's go ahead and find our document input component and let's go ahead and do this. Title prop will be data.title and ID will be data ID like this. And now let's go inside of the document input and let's properly develop this method. So interface document input props will have a title, which is a string and an ID, which is a type of ID of documents. Now let's go ahead and use this here. So we have title and ID here. And now inside of here, what we can do is we can render the title like this. So now in each of your documents, you should actually, there we go. Now we have a document loading here. Let me just refresh. Okay. It seems like we're getting an error. Oh, we are not using a use client somewhere. Or we are, let's see. Okay. This is the issue. Instead of our document ID, document.dsx, this is an asynchronous component and it has use client. That cannot work together. Okay, so let me just see what I did wrong here. We have document here. Let's see, document or null. Let me just quickly confirm this. Document or null. I thought that that would not be possible because of the preloading here. How about if we add an additional check here? For example, if preloaded document was not found. And if no preloaded document, we can throw a new error here. Document not found, like this. And then I not sure if this can be undefined or not Oh yeah well it because we defined this kind of type here So I guess it can still be considered undefined But here's what we can do. We can go inside of the getById method here, and we can improve the method itself. So for example, constDocument can be this. and then in here if there is no document throw new convex error document not found and then in here return the document so this is one thing that we initially even i think had but i removed it because in our live blocks route here we also check if there's no document right But I think it's okay. An additional check will definitely not hurt us. And now you can see that we have no issue with passing that to the navbar data. And we can even do this check here or we don't have to, however you prefer. So let's go ahead and refresh. And we have room loading and it says blank document right here. So if I go and open a software development template, which will create a document which is named the software development proposal. That is my title. Great. So now we have to add an ability to rename it from this input right here. So let's go back inside of our document input right here and let's continue developing. So I'm going to go ahead and give this a value and set value from use state. And I'm going to give it default value of title like this. I will just move this two to the top and separate one from another. And then I'm going to define is error, set is error from use state false. And then I'm going to do the same thing for pending. And then I'm going to do the same thing for is editing. Like this. Editing and this will be set is editing. Okay, so all of these values can stay together like this. I will define my input ref here to be use ref with an HTML input element type and null as default. Make sure you have added the use ref import here. Then I will define my mutate method to be use mutation, which I can get from convex react. And I have to import my API from convex generated API. And we are going to prepare API documents updated by ID like this. Great. So now let's go ahead and let's use these values in our, well, div here. So if we are editing, we will show one thing. Otherwise, we're going to show this static span here like this. But if we are editing, we're going to render a form. And then we're going to render a span here with a class name, invisible, white space, free, like this, px of 1.5, and text large. So basically, we're doing a visual trick here. Because what I want to do is I want to have an input, which will expand as I type new characters for the name of my document. And there are many ways you can do that. but this is i've tried like a different solutions for this and this somehow works the best it is the most visually pleasing one everything else is kind of jittery and jumps around so inside of here we're simply going to render the value or an empty string with a space inside and then use a native html input element here and give it a ref of input ref give it a value of value give it an on change of an empty arrow string and the class name absolute inset zero text large text black px of 1.5 background of transparent and truncate like this and now let's go ahead and give this span here, an on click method, set is editing, set to true, set timeout. So since we can't really do after this, because this is asynchronous, right? So we can kind of do this hack where after we set is editing, we time out for a bit and then we attempt to focus. Because unless this is actually set the true, if we attempt to do this, it won't be able to focus on an input because it's not even rendered at that moment. So that's why we kind of time out for a bit. So let's go ahead and try this out. If I click here, oh, it looks like it did. So I guess it does somehow work, but something is very broken. Okay. Let's see. What did we forget to do? Let's go and give this form a class name relative w fit and maximum width of 50 ch like this let's refresh and let's try clicking again so i'm going to go ahead and try and click again let's just wait for this to load click again and there we go and you can see that now uh well i didn't really implement the on change yet but you can see how this becomes an input right so now this is what we are going to do we have to implement on change first so let's implement this on change method here const on change we'll get the event which will be a type of react change event html input element here const new value will be event target value set value will be the new value. And then to do the bounced value. We're going to do that in a second. So let's just apply the on change here. And let's try changing our document name. So there we go. You can see how the length of my input follows exactly how many characters I type in. And we also have, of course, a limit. And you can see how when we outfocus, it reverts back to this. Now let's go ahead and let's develop our use the bounce method. You can also install your own, but since we have this legacy depths problem, let's create our own instead of hooks called use the bounce. And this one will be based on the change event rather than use effect, right? so i asked you know chat gpt help for this so if something seems a bit unclear or weirdly formatted that's why let's export function use the bounce and now we have to write the type inside t extends spread the arguments inside which will be parameters and give it t as the argument and it will return a return type with t as the argument the first argument is a callback which is t and delay which by default will be a number of 500 or half a second let's define a timeout ref to be use ref and let's give it a type of node.js.timeout and return use callback from react like this spread the arguments which are a type of parameters p open the arrow function if timeout ref dot current clear timeout timeout ref dot current inside and then let's configure and start the timeout for the first time so set timeout here hold the callback and spread all the arguments possible inside and then add a delay and after this add callback and delay inside of the dependency array here there we go that is our use the bounce method here so now we can go back inside of the document input component and we can now add the debounced update so i'm going to do this const debounced update will be use the bounce. Let's first import it. So use the bounce from hooks, use the bounce. Get the new value, which is a type of string. If new value is identical to the current old value, we can break this method. No need to update editing. Otherwise, we're going to set is pending to true and we're going to call mutate using the ID and title will be new value here. I can just confirm we have the ID okay and then in dot then we can call toast from sonor. If you want to, you don't have to, you can decide when you want to call notifications. So toast.success document updated .catch, we can throw a .error, something went wrong and .finally we can do set is pending back to false. There we go. So now, after we set the value, we're also going to call the debounced update here and passing the new value. So we will immediately update the local state, but for the function which will actually call the API request, that request will be debounced, right? So we will not update on every keystroke. Great. So we have that developed. A couple of more things I want to add here. So let's go ahead and do the following. We have is editing. That is fine. But I also want to do on blur here. In that case, set is editing. Let set it to false like this And let also make sure to implement a handle submit method So you can just copy the debounced update and call this handle submit But it's just going to be a normal arrow function. And inside of here, we're going to have an event, react form event, HTML form element like this. And the first thing you're going to do is call prevent default. And we're simply going to update by whatever the current value is. And then you can give that to the form right here. So simply call the on submit handle submit. And in here on then, we're going to do one more thing, which is we're going to set is editing false like this. There we go. And I think that now we should be able to rename our documents. So I'm going to change this like this and I will just, there we go. It's already updated, right? So if I refresh now, there we go. But it will not be updated on every keystroke. You can see how I have to stop and then it will be updated, right? Or what I can do is I can press enter and then it will immediately be updated, right? So I can do all of those things. And when I blur out, there we go. And what's cool about it is that it will be immediately updated to the other user as well. So if I go here, this user is in blank document. Let's refresh their status a bit. If I change this to one, two, three, like this and go back here. There we go. It's back to one, two, three. Great. So it works in real time. Our whole app is so fast and real time. I'm very happy about that. And now let's add some proper cloud icons here because right now it always shows that we have a perfect connection. But there are cases where we might not have the perfect connection. So let's go ahead and do this. How about we add const status from use status from live blocks react. And we can actually remove the is error here. and for the is pending okay let's keep let's keep this pending yeah that's fine so now this is what we're going to do make sure we have imported use status from live blocks react i don't think we have to import from suspense i think i think this can work just fine so this is what we're going to do now we're going to define a few values const show loader will be if either is pending from updating the document or if status is connecting. Oops, how did I go all the way down here? Or if status is reconnecting like this. And show error will be if status is disconnected like this. And then this is what we can do here. If not show error. Let me just see. If not show error and if not show loader, in that case, we can render the BS cloud check. If show loader, we can render the loader icon and give it a class name size for animate spin and text muted foreground like this. And let's see what did I set for error. So if we are disconnected, if is error, in that case, we can add bs cloud slash icon from react icons dash bs like this. And we can give both of this a class name of size four. So we did not define is error. Oh, no, we defined show error like this. So now I think that we should have some nicer and more logical states here. So let me try. You can see while it's updating, it's kind of rotating. Great. And if you want to, you can add the is error state and then you can add it to the show error logic here. Great. So we now have that. That seems to be working great. What we have to do next is we have to enable a new document, rename and remove. And we can do that in this chapter because it won't take almost any of our time. So let's go ahead and go inside of the navbar component, inside of the individual document ID right here. And we have this new document, which we can very easily add because all it requires is just a mutation. So let's go ahead and do that. Instead of here, I'm going to add const mutation to be use mutation from convex react. We need to also import the API. So let me just move things where they should be. Okay, like this. Make sure you added your API here. So mutation will be API documents create like this. There we go. And we can define const on new document. Mutation title will be untitled document. initial content will be empty. Then we're going to get the ID. And what we're going to do is the following. First, let's get our router from useRouter from nextNavigation. So just make sure you have imported this. Is this okay? It is. And then we're going to do router.push slash documents. and then the new ID. And we can also do Toast from Sonner. Again, it's up to you how many notifications you want to show and when. So Toast.success document created. Or you can catch the error here. Toast.error. Something went wrong. Like this. and attach the on new document now to your menu item for the new document. So on click on new document. Let me refresh. And now if I click file, new document, there we go. I'm redirected to the new document and it's called untitled document. Great. So now what we have to do is we have to import the remove dialog and the rename dialog. So let's go inside the nav bar here. And let's add those two imports from our components because we made them reusable components. You should have the rename dialog and the remove dialog because we use both of those in the document menu. There we go from here. So this time we are using them inside of the navbar. So let's go ahead and do that. Let's start with the remove dialog. what we have to do is we have to find our remove and with menu bar item with remove text and trash icon here and what we're going to do is we're going to wrap the menu bar item inside of the remove dialog and we're going to pass in the document id to be data dot underscore id and what i'm going to do here is give it an on click stop propagation and i'm giving it an on select Prevent default. So now, let me refresh again. If I go and click file and I click remove, I should be able to remove my document. And there we go. You can see once I removed it, we have an error because this document is no longer found. And I can now go back to my list of documents. You can, of course, decide how you want to handle that inside of your remove dialog if you want to. on then you can redirect back to lists if that's what you prefer right you can do you can handle this however you want so now let's do the same thing for the rename rename dialogue here and let's give it the document id here to be data id and let's give it initial title to be data.title. And let's copy these two methods and let's assign them here like that. So now I'm going to go inside of a random document here. And while we can already rename it from here, it's nice to also have this reusable model here and it's immediately updated and it works perfectly. Great. So if you want to, I think it would be cool to have a router here, use router in the remove dialog, import it from next navigation. And then after you delete it successfully in the then method, when the document is removed, it might make sense to do a router.push back to the homepage. So we don't have that weird experience when I'm in a document like this. I do remove. Oh, it looks like it's not working. I guess that's because it catches the error faster then it redirects. Right. So let's go ahead and go to localhost 3000 here. And let's see, will this work? Remove? Yes. Okay. You can play around with this. See if you can somehow get it to not catch that error. I think that one thing you could do is go inside of the server, instead of documents document id page and perhaps you could remove this i think that maybe that could give you a slightly better experience let's see it's a loading document loading the room and if i remove it okay looks like it still uh catches it okay so uh what we uh can do next besides the deployment is we can go through our code, look for to do. And what we can do in the next chapter is add initial content, though that is very, very simple to implement. But I will also look and see, is there anything else important that we can add? Great, great job. In order to implement templates and some initial content, we have to do a couple of changes. Starting with our constants, instead of source, we have constants templates right here. So what we have to do is we have to add initial content to each of these. And I recommend you use backticks like this. And then what you can do is simply enter any kind of initial content using HTML You can also use inline styles if you prefer A good friend of yours is ChatGPT who you can feed this style of document to and tell it okay now create me a software proposal, now create me a project proposal and stuff like that. Another way you can get, except typing it out yourself like this, you can literally create a new document. You know, We can go ahead and create a project proposal for name like this. And then make a list. And then you can insert a table. And you can go ahead and click file and save this as HTML. And then you can add that HTML in the initial content. So what I'm going to do now is, well, it doesn't make sense. for you to watch me type out the initial content. What I'm going to do is I'm going to add something I have for project proposal here. You can pause the screen if you really want to type it out, but this is just very, very basic HTML, nothing special here. Same for the business letter, some basic things for the resume. I'm also going to just do some basic things. You can, if you really want to pause the screen and follow it along, but you can just copy the same thing from all of these others and then edit it later to your preferences. And the last one here that I have is the letter. There we go. So now we have the initial content in each of our templates here, except this one. If you want to, you can add it here and just set it to be empty like this. So what we have to do now is we have to go inside of our app folder, home, and templates gallery right here. And now each of our template will have the second argument, which is the template.initialContent like this. So our on template click will now store inside of the database with the initial content. I seem to be having an error here, and that is because I have to restart my TypeScript server. There we go. Now there's no issues. So right now what I'm going to do is I'm just going to show you this with data in convex. So let me just delete all of my documents now. I don't need them. So for example, I'm going to add a random template. And if you take a look inside of my data, I have initial content inside. So this part works. What doesn't work is actually loading that data here. So for that, we have to go a step further and go inside of source, app, documents, document, ID and go inside of the editor. And now we have to modify our editor to accept initial content. So let's create an interface editor props initial content and give it a string or undefined. And then inside of here, go ahead and add editor props initial content like this. And then in the use live blocks extension, you can add the initial content. Just like that. Now, if you just refresh this existing document, it will not magically populate it because this was created before we enabled initial content. So you can feel free to delete this document. But from now on, every other one that you create will have some initial content inside. Or it won't. Okay, let's see. I forgot to do another thing, which is inside of my document ID, document.dsx. In the editor, initial content comes from document, initial content. I was too confident on the first try, but there we go. You can see it now. So let's try this one. There we go. Software development proposal right here. And if I, for example, change this to software engineering proposal, It will save this, right? So I can exit this. I can go back inside. And it will not confuse that initial content. You can see how it holds my change because originally it said software development proposal, right? But there is this slight little issue that I'm noticing with all of my documents. And that is that they are all, when I create them, it loads. And then there's like this moment of, it's kind of like it's waiting to synchronize. You can see how this was loading for a second. What you can do to improve this is you can add the offline support underscore experimental and set it to true. In the future, this might just be called offline support. Or if it's not, you can just visit the documentation. But again, if you're using the same versions as I have shown you, you will have this option. And this should drastically improve it. You can see how now it's blazing fast. Let's try with this. There we go. You can see how fast it is now. It is very, very fast now because it uses index database and actually loads the content immediately. Great. So we have improved this a lot. You can improve it even further by reducing the amount of this room loading time. So this room loading time can be reduced by adding proper client suspenses on specific places. So you have to go through your document ID here and basically have to find all things which use live blocks suspense. For example, my avatars are for this reason encapsulated in their own suspense. So they don't take time in the loading room screen. Let's see. My inbox is also encapsulated in a client-side suspense. My room here is in itself a client-side suspense. So that's fine. We seem to also have threads here. So this could be something we can resolve. Let's try this. let me just or maybe we are already encapsulating this i'm not sure let's see in my editor here okay we just load the threads here so perhaps this is what we can do we can call this threads threads list and then export const threads will be client side suspense and inside threads list like this. Maybe we can do that. And then we don't have to export this. And we can change the props to be the same here like this. We can add the fallback to this to be null. And we can pass in the editor here to the editor like this. So now this in itself is inside a client suspense here. So perhaps that could maybe improve it. There we go. You can see that now we didn't even see the room loading. You can see it's very, very fast now. Extremely fast application. You can see how fast. And this is in development mode. It's going to be even faster when we switch to production. And all of these things will and can get cached even further. Great. So I'm extremely satisfied with this speed. also with searching. Everything is blazing fast. Amazing. So I think we have covered pretty much all the features here. I will, of course, take a look again at the app once again after this chapter. But here's one thing that I really wanted to do but never got the time to. It's about the number 56. Specifically when we use it like these magic numbers in paddings and in the room here and in the ruler. It's just a magic number that I don't like and I feel like it can easily be misplaced. So let's go inside of source constants and let's add margins.ts and let's export const left margin default. to be 56. And let's export const right margin default to be 56 as well. And let's start with our ruler. So I'm going to import both of those here. So import right margin default and left margin default. And now let's use it here. So in here, we have left margin default and right margin default. I basically don't want to see the number 56 anywhere. So left margin default and right margin default like that. So I think that inside of here, 56 is no longer a number. Let's see where else in the editor component itself. When we add the style here, let's import all of those. So left margin default and right margin default. So inside of here, what we can do is this. I think we can do that here. Left margin default and right margin default. So then we don't even have to do this. Great. Let's see where else in the room component. So let's do it here as well. I'm going to import left margin default and right margin default. And I will add it in this room here. Left margin default, right margin default. There we go. So now let's see if everything still works as intended. And I will center this so we can see it. There we go. Everything still works as intended. it. Great. So if you want to, I would recommend, you know, I'm making this for, you know, a tutorial. So obviously some things here are going to slip, but you know, there are things which you can improve. For example, this magic numbers, 816, it's a pretty important number because we rely on it a lot. We use 816 here, here, basically it's the size of our paper, right? And a lot of calculations depend on that number. So it would make sense. I stored it here in page width, so I guess that's good. But it would be good if we also stored that in its own constant. So I challenge you to do that as well. Search for 816 and look through everywhere where\nIt exists. You can even search for PX, right? Make sure you handle all the cases. Great. So I personally think that we are ready to deploy the project. I will, of course, take a look once more to see if I have forgotten something. Nevertheless, amazing, amazing job. So one thing that I just thought of is that right now, both of my cursors have the same color, right? And it will be nice to differentiate these users based on the color. And you can actually do this quite easily by going inside of the API Live Blocks Out, where we add the session. And inside of User Info, you can just add color, for example, green. And ignore the TypeScript error for now. But if you refresh, you can see that cursors are now green. So what you can do is the following. const name and replace this inside. And then what you can do is const color and make it the following. So we can do this. We can do name to number, get name dot split, reduce, get the accumulator and the character. and now we're basically creating a unique color for each username. So this is chat GPT assisted. So what I'm going to do is I'm going to add the hue here and we can get the hue by using math absolute name to number. Add modulus 360. I'm going to be honest, I don't know why this works or how it works. but I got some nice results for this. Basically, every username will have a unique color. So now we have the color and we can just set the color like this. So basically, this is the code we have added. And you can also replace the name here with the name. This will still have an error, so that's okay. But basically, this is our new code. So we are using the username to create a unique number. and then we get the hue and then we use HLS, HSL to create a unique color. So let me try refreshing both pages here. And let's see if now each name gets their own unique color and they do. So you can see now each of my names here has their own unique color. If you wanted, there are a billion ways you can do this. You can create a list of colors and then, you know, use math random depending on something from the user to get the color. And to get rid of the TypeScript error here, you go inside of LiveLogsConfig, and instead of your user meta, you allow color to be a string as well. And there we go. Now you have solved that error. Great. So what I want to do now is I want to see if we have any important things to do here. So we have this duplicate extension name found image. This can lead to images, so let's explore that. We have the extension image right here, and we have added it here, and it's telling me that that's a duplicate extension. So it could be because of the imagery size maybe, or it could be because of tip-tap starter kit. So let me go inside of functionality, starter kit. No, it doesn't have it here. So I'm not really sure where does the duplicate extension come from. But let's try. If I console out image resize, maybe then it works. Looks like that was it. Looks like the image resize already includes the image extension. So if you want to, if this becomes problematic, you can just turn off the image resize from your extensions. But mine was working completely fine. What we have to do next in order to prepare this for deployment is fix build errors. And we do have some. Basically, if you go inside of your components UI, for example, if you choose input, you will see some type errors here or maybe not. Maybe they have been resolved in the current version of ShadCN. But previously, there were a bunch of build errors inside of there. So this is what we will do. We will close everything and we're going to go ahead and focus on this. How would we do npm run build? So what we're going to do now is basically looking if there are any errors in our build. So I'm going to pause and show you if we got any errors. There we go. So we do have some errors and we can go ahead and resolve them one by one starting with home navbar so inside of my source app home navbar i have imported organization but i never used it so i removed that maybe i added that accidentally in documents documented the avatars this is a warning so i don't think this is breaking my code. I think this is perfectly okay. Let's go inside of components UI calendar, which we are not even using. So if you want to, you can delete it, but you can also resolve it. So components UI, we have the calendar here. I'm not sure what is the error. So props is defined, but it's never used. For some reason, my TypeScript server is not even showing me that this is an error. So let me see what lines even are those props here, maybe some lines here. Oh, this one's okay. I can see. So yes, all the way down here, if you want, you can remove them and then this should solve the calendar. Then let's go to the chart or you can just remove it. right let's go to chart instead of here i have this entire unused import i think that's the only issue here uh no we also have this underscore is unused let's see where is that oh i see it's here in the chart style so basically in the chart style we have to be careful here because this is an array so it needs the second item in the array and what we can do is just remove this and just leave a comma. It's important that you leave a comma here. So I think that resolves this in chart. So line 77, let's see. Yeah. Oh, 77. I guess that's the same thing. Okay. And we have use toast where action types is assigned a value, but only used as a type. So let's see. Use toast inside of hooks. action values where are the action action types is assigned a value but only used as a type here I don't know can I add yes ignore here let's go ahead and try and running run build again looks like it's throwing instead of use those again to use T.S. expect error instead of T.S. ignore. Okay. Oh, and now this is throwing me something else. Unused expect error. How about we try just disabling everything here? slink disable add typescript dash slink dash no unused vars. Not sure if this will resolve it. Let's see. looks like we have some new issues now because we added that color thing so instead of a room.tsx let's try that so instead of source app folder documents document id room.tsx here is now throwing an error here because I think it's because I need to modify my user type here to include a color string. And in my get users here, I'm not giving them a color. So I guess we also need to add a color from here. So let's go inside of our live blocks out here. Let's copy this entire function here. Let's just add it here. and let's see what's up with this user. Okay, I see. I see the issue here. Can we just put an empty string for the color here? Will that resolve it? Let's see. Will our app still work? Well, let's first try building it and see if that's okay now. All right, there we go. So it officially built our app with no more errors. So let's see everything that I had to change. Well, first we added the color here. In the navbar, I removed the unused organization. This is a whole other story where we added the unique color for name In my actions for my get users action I had to add the color to make the TypeScript happy But I don think this color will actually be used anywhere In my room here, I had to assign the color type to the user because otherwise this component fails TypeScript. In my calendar, in the components icon left, icon right, so this is all the way to the bottom of this component, I had to remove the unused spread of the props here. in my chart, I had to remove this unused import. And I also had to modify this unused underscore here and just leave an empty comma here. So make sure it uses the second argument for the calendar and chart. You can just remove them if you don't want to fix them because you're not using them. And then use toast. I've just added a big Eslind disable Eslind disable TypeScript S and slash no unused words like this. So those are all the changes I did. Great. So now let's go ahead and let's deploy our application. So the first thing you have to do is you have to create a new GitHub repository. So go ahead and call this docs project tutorial, whatever you want to call it, set it to public or private, whatever you want, and click create repository. After that, since this is an existing repository, you're going to copy these three lines here. And what you're going to do here is you're going to get add and get commit and write whatever get commit message you want. For me, I do a number after each chapter that I record. So for me, this is 31 deployment like this. And then what you have to do once you have committed your changes is you have to run this three commands right here and press enter. And then you will have your new repository here. There we go. So 31 deployment is my most recent deployment here. And now let's go ahead and deploy this to Vercel. So create an account on Vercel, find the new project button, and it should automatically if you log in with GitHub, it should automatically fetch all of your projects here. So I recommend that you connect your GitHub. And there we go. This is my project. So I will click import right here. And now what we have to do is we have to follow a convex tutorial on deploying to Vercel. So we created the account, we linked. And what we have to do now is we have to override the build command. So let's go ahead and do that. Open the build and output settings, turn on build command remove what's inside and then make sure you add this inside great and now for the environment variables inside of here we have to add the convex deploy key so let's just prepare it inside convex deploy key and we're going to using we're going to be using production now so let's go inside of our convex dashboard here and let's switch from so this is my docs tutorial project and switch from development to production. So this should now, you can see it's creating it for the first time for me. And now let's go ahead and let's go inside of settings here. And let's go and find my deploy key. There we go. Generate production deploy key. And I'm going to call this Vercel and let's click save. And I'm going to copy this and then I'm going to assign it right here. There we go. So now what we ought to do is add all of the other environment variables which we have. So instead of .environment.local, you can copy all of these. You can click add more and you can just paste and all of them will be added here. So I'm pretty sure you don't need convex deployment. I'm also pretty sure you don't need a next public convex URL because I think all that is handled with this convex deploy key and this will be overridden with npx convex deploy command so i think you don't need it so actually i will remove those i will remove this as well so i will leave the next public clerk publishable key clerk secret key and live blocks secret key but we can do a step further here we can go and switch live blocks to production as well so let me switch that to production. Let me go inside of my API keys here. I will click generate key for my secret key. And there we go. I will copy it. And then I will replace whatever I have here with my actual production key. And I can do the same thing for clerk here. So let's see how to do this. So inside of here in my project, I will click on create production instance, and I will click clone development instance. And let's double check. And let's see if we can actually do this here for Vercel. So yeah, it looks like we first have to deploy. So we will see if we can use if we can use a Vercel domain So for now I will leave it as it is like this And let click deploy So I didn change clerk secret key It exactly as the one we had here The same with next public clerk publishable key because I not sure if Vercel domains are allowed. I think you might have to have a custom domain, but we are more certainly going to test it, but we first need to provide them with any kind of domain. Okay. So my build seems to have failed here because of peer dependencies here. So I will be looking into this. I think I know how we can resolve it. So I'm going to go to my project here, my settings. Let's see. I have my install command here. So we're going to do npm install dash dash legacy here depths let's add that and click save here if that doesn't work well we can just switch to bun if nothing else works so let's go ahead inside of our deployments now and let's click redeploy and click redeploy again and let's see if now we'll have any better luck there we go looks like that command has fixed my problem those of you that are using bun or yarn or pnpm you don't have to do that change because you are running a different command you're most likely running bun install which means that everything for you works out of the box so i'm very jealous for you uh let's go ahead and visit this so we now have this is your domain always make sure to use this short domain. Don't use this long ones. So let's see if firsthand everything is working or is everything broken here. So I'm going to go ahead and log in into one of my accounts here. And it looks fine. Let's create a new document. Immediately created. And here we are. Amazing. Let's see. Can I rename the document? I can do that as well. Great. Let's see if one of our templates here will work. It will and blazingly fast. So let's go ahead and test out an organization. So I'm going to go ahead and split screen and see if collaboration is working as intended. There we go. So both of these are now on my production. And let's try and see. and it looks like it is working perfectly. Let's try one more time to see. It looks like both of these appear here. Hello world. Let's try typing here. Hello there. Amazing. Let's try tagging each other. That seems to be working as well. So let me try going back. There we go. Notifications are working. Everything seems to be working just fine. Amazing, amazing job. So I think what we can now try, since we now have the main, which is Docs Tutorial Vercel app, if you want to, you can try and switching to production here. So click clone. And let's see, can I add Vercel app? looks like it cannot contain that. Let's see now. Yeah, Vercel app domain cannot be used to deploy production apps, so they want to reduce spam. But for demonstration purposes, development will be more than enough for you, so don't worry. But if you do later create an actual domain that you can enter inside, be sure to switch to production here as well. Great. Amazing, amazing job. We've battled with Next15 React 19 release candidates and still we managed to fix every single issue that we came across and we built an amazing collaborative product in the end. Dare I say also a blazing fast product in the end. Amazing, amazing job and see you in the next tutorial. One last thing that we can do to improve our project is to add autofocus. Because right now when I add a blank document, I have to click and then I have to start typing. But TipTap, Use Editor offers autofocus, which you can set to true. And you can see that even though it's a very small change, it greatly improves the experience and gives you that Google Docs or Microsoft Word feel when you immediately have access and you can focus on your document the moment you open it. so go ahead and do that change and this is great lesson for me to also teach you how to redeploy so all you have to do is git add git commit for me this will be 32 out of focus and do git push and that is it that's the entire thing you have to do you can go inside of your oversell now and you can see in your deployments, I have a new build building here. And then it will be active for everyone to see on your main domain right here. Amazing, amazing job.",
  "transcript_chars": 461317,
  "transcript_filled_at": "2026-06-06T15:46:33.590779+00:00",
  "transcript_filled_by": "tk-bulk-groq-retry-20260606"
}