In this chapter, we're going to develop our IDE layout. We're going to start by creating a dynamic project routing system. Then we're going to build resizable IDE split panes using a package called allotment. We're going to implement a navbar with project actions, set up editor and preview containers and add code preview tab switching functionality. So how is that going to look like?
Well we have a finished example here. So this is the finished product. We don't have this yet and we're going to start developing some things here now. So let me go ahead and show you what the first thing we're going to do is this knob bar right here or at least we're going to prepare this navbar right so that's what we're going to implement and then we're going to implement the basic split pane this will be pane on the left side and this will be the pain on the right side. And besides that we're going to implement the ability to switch between code and preview.
But we're not actually going to implement any of the contents inside, right? So let's go ahead and start building. Make sure you have your app running at npm run dev. And what we've built last is this the landing page. And currently, if we try to go to a project we get a 404 so let's start with resolving that and let's also get rid of our user button on every single screen.
So I'm going to go ahead inside of source components providers I will remove the user button from the authenticated state and I will remove it from the imports. Speaking of unused imports I believe there's one inside of app layout at least I have it So I'm just gonna remove it. Great. So now we have a clean 404. Now let's fix it.
So how do we do that? Well, let's take a look at our URL. Localhost 3000, projects, and here's the tricky part. We now have something dynamic. So far, we've only learned how to create the URLs when we know exactly the words, But it's not difficult to make it dynamic either.
So, we already know the first part. Inside of the app folder, create a folder called projects. And now, what do we put inside? Well, it's actually very simple. Open square brackets and then type the name of the variable where you want to store the param.
In our case, that's going to be project ID with a capitalized letter I. And then inside, add a page.tsx. And you will probably notice that you will now get a different error here. That's because this is a reserved file name. So it officially registered this route and we are hitting that route here.
But the problem is page is not exporting anything. So it's not really not found anymore, it's just incorrect. That's why we have to export something. Project ID page. Div and let's prepare just a label which is supposed to show the project ID which we've entered.
So how do we access this project ID now? Well, through params. So let's go ahead and prepare the params here and let's give it a type. A type of params is a promise and then inside what variable we expect. Well, we expect project ID.
So let's go ahead and add it here and let's give it a type of string. And then let's go ahead and make this entire thing asynchronous like this. And let's destructure project ID from await params. And let's render project ID. There we go.
So now no matter what you type in your URL like 1, 2, 3, 4 you should still get the same result. See? It should be immediately reflected. The reason I explicitly mentioned with a capital letter I is because people often overlook this. If I rename this to something like this and update the imports you don't have to do this you can see it just causes a bunch of cache issues right and if I refresh here looks like it's actually still working that's interesting I'm interested now because I wanted to teach you a lesson but maybe I actually learned something new.
So I'm purposely deleting .next to get rid of the cache because what I'm expecting here is that this shouldn't load and it doesn't. Okay, so it was just cache, right? Basically, if you are not careful with how you name your folder, you will probably write the wrong variable here. That's what I was trying to tell you. That's why it's important to do that.
So if you wanted to fix this, You would now have to use lowercase project ID. You can see that resolves it, but we don't want to use that. We want to use the proper camel case. So because of that, I'm going to rename it once more. And then again, I'm going to have to update my imports here and most likely.
Remove cache and run it again. Let's restart and we should be good to go. There we go. Now let's go ahead and let's create the layout. So layout is also a reserved file name here.
And the reason we're using layout is because layout will not re-render on every project ID change. But we are kind of going to need to have it to be static. It's kind of easier to explain if we just start building it. So it's quite similar. Let's go ahead and do layout here.
And well, for now, let's just render the children, because that's kind of the only important thing. We've never written a custom layout before so I will stop to explain a bit. So the type will be React React Node and that's not where I type this. My apologies. This is where I type that.
And just by saving everything should work perfectly fine, right? So what's the purpose of a layout? Why did I just do this? First of all, it's a reserved file name, so make sure you didn't misspell it. Second of all, it's going to be used like this.
Imagine this is a navbar and I know it doesn't seem like much yet, but if I create another thing here, for example, settings page, right? So imagine each project has a settings page and if I add page inside, you don't have to do this, I'm just trying to demonstrate. Settings page, div, whoops, project settings page. How do I access this now? So I go, take a look at the URL.
Projects1234, forward slash settings. That's how I access it. What are you noticing here? So this content has changed, but the layout content hasn't changed. Right?
So I'm just being semantically correct when it comes to the things I want to put in my layout and the things I want to keep in the page. The layouts are also less prone to re-rendering, making the app more optimized. For now, we can get rid of the settings one. We are not going to need it. I just wanted to explain to you in that way.
So yes, let's go back to just projects and then 1, 2, 3, 4. Great. Let's now focus on this. Layout.tsx and we can actually access the params here as well and they are the exact same type so let's add them there we go and let's make sure layout is asynchronous as well and let's destructure it and let's change our return to very simply be project ID layout which we don't yet have and render children inside Let me go ahead and add parentheses around this so it's easier to look at. And the only thing we're going to pass as a prop will be the project ID.
So when I save, we're going to get an error because project ID layout doesn't exist. So let's go ahead and create it. Inside of source features project components. Let's create project ID layout dot TSX. I'm going to mark this as use client.
And then I'm going to export const project ID layout. The children, the props I will expect, whoops, the props I expect are children and the project ID. Children are a type of react react node and project ID is a type of string. And then let's go ahead and return a div and let's just render the children so we know it's working. Now let's go ahead inside of app projects layout and let's import project ID layout from features projects components project ID layout.
And since I can see my children it means everything is correct. We can now focus exclusively on the project id layout. Inside of the project id layout we're going to go ahead and give this container div a class name of full width height of screen flex and flex column And then we're going to implement an actual navbar component and pass along the project id. Let's go ahead and let's implement the navbar component. So again, inside of projects, let's create a new file called navbar.dsx and I'm going to import a type of id from convex generated data model.
I will export const navbar. I'm going to prepare the types here, so we expect the project ID and I'm going to do something different. I'm going to give it a type of ID projects. The reason I'm doing this is so that I can easily pass it to convex queries. So in order to make this work, first of all, let's make sure we return something.
So, hello navbar. Then let's go back to project ID layout. Let's import the navbar from .slash navbar and you can save and it will work so we now have hello navbar here but the project ID is incorrect so we're just gonna do the same thing here. Instead of project ID layout give this a type of ID projects. ID is also a string but we are just making sure that it works as an identifier for convex.
Great. So now that we have this project ID, let's go ahead and let's style this a bit. We are going to use project ID later. For now let's give this a flex, justify between, items, center, gap, x2, padding 2, background color of sidebar, border, bottom, and border b, border, actually we don't need that, We can just do border bottom. And then in here, let's create a div with a class name flex items center and gap x2.
Let's change the outer one to nav actually. I think that makes more sense, right? Now what we have to do is we have to import all the components from ShadCNN navbar. So that's gonna be breadcrumb. My apologies, from ShadCNN Breadcrumb, not Navbar.
So Breadcrumb, item, link, list, page, and separator all from components UI Breadcrumb. And once we have that, we're just going to create a composition to render the breadcrumb. So starting with the actual breadcrumb, then inside we're going to add a breadcrumb item. Then we're going to add a breadcrumb list with a class name and another property. So let me just go ahead and properly indent this.
So we have a class name flex-items-center-gap-1.5 and group forward slash logo. In fact, we don't need group logo. Just this. And an as-child prop. And inside of here, we're going to render a button.
Make sure you import button from components UI button so it's chat CN button. The button will have a variant of Ghost. It will have a class name of withFit with an exclamation mark. This basically means important, like override whatever other style it had. Same thing for padding and same thing for the height.
And as child here as well. Then finally inside we're going to import link from next link and give it an href to go to a well to the root page. So just make sure you've imported link from next link. Great. So what will actually be displayed here?
Well, an image. So let's import next image. Let me show you what the import looks like. Just a second. There we go.
Next image and I will put it right next to link. The image will have an href, my apologies, a source of forward slash logo. We are going to have an alt of logo, width of 20 and a height of 20 as well. And then next to the image we're going to have a span polaris. Let's go ahead and give this a class name and let's use the CN util library.
So in the first argument here we're going to put text small and font medium. And then what I'm going to do is I'm going to go back to projects dash view component and in here I'm just going to copy the popins instance and then I'm going to go back inside of my navbar here I'm going to paste this and I'm going to import popins from next font Google and I'm just going to move it here at the top. So now we have the poppins font again so I can now add a comma and then in the second argument of the CNUtil font.classname making this have the Poppins font. Great. So that's the first item.
And now let's go ahead and add a breadcrumb separator. And actually it's a self-closing tag. The only class name we're gonna give it is ML-0 and MR of 1. And then let's add a breadcrumb item again here. So what should we write here?
Well, we should use the breadcrumb page. That's the one we should use. And inside of here for now, let's just do demo project. And for the class name, let's give it a text small, cursor, pointer, hover, text, primary, font, medium, maximum width of 40 and truncate. Let me go ahead and zoom out so we can see how this looks like.
So looks like it's not taking a hundred percent height. I apologize. With looks like it's not taking a hundred percent with so let's debug why. Okay that works. Let me go back here.
Yes this and this should definitely expand to a hundred percent with. So let me just go ahead and see why that is not happening. Oh, it is because I gave you an invalid composition. So instead of breadcrumb, we should have breadcrumb list. We're not using that.
So let's go ahead and encapsulate both of our breadcrumb items within that list and then let's indent everything together. There we go. And I'm going to give this a class name of gap0. Alright. So let's take a look now.
This is our navbar. In here we can see we have what is what will be the test the text of the currently loaded project And in here we have a button to go back, right? So our next step is to actually load the name of the project that will be here. But before we do that, we can actually do one easy thing. So let's go at the bottom here.
And in here, let's just add a class name, flex items center and gap to. And just render user button from clerk next JS. So make sure you've imported user button from Clark Next.js. And now you have a place to log out. So if you need to change your account, you can just go ahead and log out from here.
Great. So let's see what's next. So we can use the project id to actually load the project. In order to do that we have to go inside of convex projects.ts and we have to develop getById. So I'm going to copy the existing get and I will rename it to get by ID.
The arguments it's going to accept is ID which will be a type of VID projects. So now that we have the arguments we also have to extract them here. And the first thing we're going to do is just attempt to get the project using await context database get projects and then pass in arguments.id. That's the first thing we're going to do. Then we're going to add an if check if the project is missing and we're going to throw a new error here project not found that's the first check the second check will confirm if project.ownerId is not identical to the current identity.subject meaning we have to throw a new error unauthorized access to this project because this user should not be looking at this project.
If all of these cases pass we can safely return the project. You might notice that, I'm not sure how well you know Convex, but they've undergone a change recently. This also works just fine, because their IDs are kind of special. You can see you have to define that it's an ID of the project. So in the past, you just passed arguments.id and it immediately knew it has to load project.
They've done an update where you can be more explicit and it's super cool they made this backwards compatible so both work but this is the new way so you should be writing it like this. As far as I understand the reason they're doing this is A to be more explicit and B to enable local running of convex because right now this ID thing is probably related to the cloud. So if you want to enable someone to run convex 100% locally, they have to fix that first. So they are working towards that. And I kind of like this explicitness more.
Great, so we have the GetBy ID. Now let's go ahead inside of source, features, projects, hooks, use projects. And inside of here let's implement a super simple hook use project. So make sure to not name it the same. It's use project.
It will accept one argument, project ID, which is a type of ID project. It will return use query API dot projects dot get by ID with the argument ID of project ID. So this is the new route that we have just created right And we only accept that. So make sure you have this and click save. Great.
Now let's go ahead and go back inside of the navbar .tsx And let's load the project. And it should be fairly easy now that we have the hook. So project from use project and pass in the project ID. Make sure to import use project from hooks, use projects. There we go.
And we should probably mark navbar as use client. So let's do that, use client. There we go. That will get rid of the error. Or not.
Because that's not what the error is. The error is because we added a new function, but we didn't run or should I say aren't running actively and PX convex dev. So make sure you have convex functions ready here and let's do one more refresh. There we go. But still we're not using this project anywhere.
So now let's go ahead and scroll down to demo project breadcrumb page and do project question mark name or loading. There we go. You can see that for a brief second it's gonna say loading and then it will change to the name. So try with your project and you should see different name for every project. Excellent.
So now we have to add the ability to rename this and that will be a little bit more tricky but not too difficult. Don't worry. I want to start by actually building the rename mutation. So let's head back inside of convex projects. I'm going to copy getById because it's very similar.
I'm going to rename it to rename and it will accept a name and it will accept an ID and a new name which will be a type of string. There we go. First things first, we do the identity check, we fetch the project, we confirm it exists, we confirm we have access to it and then what we do is well we don't have to return anything let's just do a wait context database patch and again you can just pass arguments.id but I like this more and this is the new way so get used to doing this. Let's pass in the new name and let's move the updated ad to be date.now. Alright, so let's see.
Yes, why does it not exist? Because this isn't a query, This is a mutation. So make sure you change this and we have it imported from here. Great. And now it should work just fine.
Perfect. I believe it returns a project ID back. No, it doesn't. Okay. No need to return anything.
So we now have the rename mutation. Perfect. Now let's add it to our hook. Use projects. Here it is.
So I'm going to go ahead and I'm going to copy use create project because it's more similar to that than anything else. So I now have copied the use create project and I'm going to change it. So the argument we're going to accept is project ID. The use mutation we're going to call is projects.rename. And let's do optimistic update here as well.
So instead of calling existing projects, we're going to change this to call an individual existing project right so let me just go ahead and see I'm trying to make this easy to look at okay so get query api projects get by id which we previously developed and pass in the argument ID project ID. I think I can collapse this like so. Just trying to make it easier to read. Okay. So now that we have the existing project, let's go ahead and check how we should actually update this.
So I'm just gonna remove everything inside. We're going to check if we have the existing project meaning it is not undefined And if existing project is not null, right? Because undefined means loading and null means not found. Only then can we call our local store setQuery API.projects getById id, project id, so we are going to find it in the local cache And then we're just going to spread the existing project and change the name. And we can also change updatedAdd to be date.now which isn't exactly correct because it's going to be different on the server but good enough.
So that's for updating the existing project but we're still not done because now we have to push it to a list of our existing projects. So for that I believe we can copy this from use create project. Yes, we can rename this to use rename project. So we get rid of the error. So after this first if block here, let's again fetch for the existing projects, check if existing projects are not loading, and then call local store set query api.projects.get, skip the arguments, go inside of existing projects, existing projects, so be careful, right, .map, get the individual project and in here return project underscore ID matches arguments ID.
If it does spread the project property change the name to arguments dot name and updated add to date dot now Otherwise just return the plain project. There we go. So I believe that is it. That's what we have to develop here. So this will change both the query for getById and also it will update the project in all the places where we fetch projects.
So a powerful optimistic update. We now have useRenameProject. But what we have to develop is the UI so that when the user clicks on this it changes to an input. So I'm gonna go back inside of navbar.tsx and I'm gonna start developing that. So first I'm gonna add our new method renameProject, use renameProject and pass in the project ID.
Make sure to import use rename project. Great. That's the first thing. Then let's prepare two states. Is renaming and name and then just import use state from react I'm going to move it here to the top okay so we should now have rename project is renaming and name as well as its setters.
Now that we have that, let's go ahead and see what should we render when we are renaming and what should we render when we are not. So down here, I'm going to find breadcrumb page and actually I'm going to focus on the breadcrumb item. So if is renaming I will render a native input. Otherwise I'm going to render a breadcrumb page. So let's indent this.
There we go. And on the breadcrumb page, let's give it an on click handle start rename. And now let's go ahead and just develop this method real quick. So. Constant.
Handle start to rename. We'll check if we don't have a project and immediately break and then call set name with project dot name and set is renaming the true. So in case project hasn't loaded we're just going to break the method. Otherwise we're going to set the local name to be project's name that we loaded and then we're just going to change set is renaming to true. What this will do is it will render the input.
And inside of this input let's see what we have to do. So I want to auto focus on it. I want to give it a type of text, value of name. On change. Let's get the event and call set name event target value.
On focus event current target select. On blur for now an empty arrow function. On key down an empty arrow function as well. And now let's focus on the class names here. So we're going to have text small BG transparent and text foreground.
Then We're going to have outline none, focus ring one, focus ring inset, and then the last few, focus ring ring, font medium, maximum width of 40, and truncate. So feel free to pause the screen and confirm you have the entire class name here and let's test it out. So now when I click here it turns into an input but currently it doesn't really do much, right? We cannot submit, we cannot cancel and when we refresh we can very well see that it wasn't saved. So let's implement the functions we need to actually save this.
We have only implemented handleStartRename so far, so now let's do const handleSubmit. The first thing we're gonna do is set isRenaming to false And then let's go ahead and trim the name. So name dot trim. If this new trimmed name doesn't exist, meaning it's falsy, meaning it was just a blank space which we don't accept as a name, or if trimmed name is exactly the same as current project.name and to fix this we can just check if there is no project return there we go break the function as well So we're not gonna waste resources updating to the same thing. Let's call rename project, give it an ID of project ID and name of trimmed name.
Great. And now let's go ahead and implement a super simple handle key down. So handle key down will accept an event, which is a react keyboard event. If that key pressed is enter, we're going to call our previously created handle submit. Otherwise, if the key is escape, we are simply going to toggle off the renaming functionality.
Now that we have both of them, let's go ahead and add them to on blur and on key down. So right here. There we go. So let's refresh for good luck and let's change this to test and press enter and you can see it's immediately optimistically updated, right? So optimistic update.
Immediately. You don't even feel that it's loading. And you can see it's reflected here as well. So, definitely works. Amazing job.
So, that's that finished. There is one more element left to develop here. And that is the import status indicator. In order to implement the status indicator, we first have to import all the components from tooltip. So tooltip, content and trigger.
Then let's go down here and after the breadcrumb ends, but still inside of this div let's go ahead and check if project.importStatus is importing, in that case let's render a tooltip, let's render tooltip trigger, let's add as child prop, and let's add a loader icon from Lucid React. So make sure you add this in. I'm going to move it here. The loader icon will have a class name, size 4, text muted foreground, and animate spin. Let me just write the alternative here so I don't get that error, even though I still have the error.
Okay. So that's the tooltip trigger and the tooltip content will simply say importing. And now for the alternative, let's check If project?updatedAt exists, even though it should always exist, though it is set at undefined. Let me check. UpdatedAt is always a number.
Okay, yeah, we can do it like this. It's okay. Copy the tooltip. Paste it inside. The trigger will be a little bit different.
The trigger will be a cloud check icon. From Lucid React. The class name will be identical besides, I mean except animate spin like that. And then the tooltip content will tell the user when was the last save. So, saved, add this space, format distance to now, from date FNS, so make sure you import that.
We installed this package in the previous chapter. In the first argument, pass in project updated at and in the second one add suffix to true. So let's check it out. Next to our name, you can see that it says saved 5 minutes ago. So if I update it, saved less than a minute ago.
Let's try and find some of my projects which are importing. Do I have any? I do not. So I'm going to go to dashboard convex.dev and I'm just going to purposely change one. So here in my database, I'm going to find one project, maybe this 123 so it's easy to find, and I'm going to change its import status to importing.
So there we go. You can see how it's changed here and when I click here, you can see it says importing. It's a little bit twitchy, but okay. Let's leave it like this for now. That's it for the navbar.
Our navbar is now finished. We can now focus on the bottom half, which will basically have an allotment pane to separate the items. So that's it for the navbar. Let's go ahead. I guess the only thing that's kind of worrying me is this syntax.
It feels weird. I'm not sure it needs to be like this. I think we can render it regardless and then in here do the thing. So if project Question mark updated at then use format distance to now. Otherwise unknown.
I guess that's like an edge case. Weird syntax but let me see if I can somehow make it easier for you to read. Okay. And just move this here. Okay.
I guess you can recognize this, right? If we have project updated at we format its distance, otherwise unknown. We have no idea when it was last updated because we don't have that data, which would be very weird because we always have the updated ad property. Cool. Or How about loading?
Yeah, how about we... Okay, I'm doing too much but you can do whatever you want. This is an edge case that should almost never appear. Great. Now that we have that, let's focus back inside of our projects components project ID layout.
What we have to do now is we have to install a package called allotment. So npm install allotment. And I'm going to show you what version I'm using. So package.json allotment. This is my version in case you want to use the same.
And this is its npm page. So what's cool about allotment is that it has an industry standard look and feel. So if you like VS codes split view implementation, you are in luck because this component is derived from the same code base. And I actually like this more than I like ShadCN's resizable component because Those resizable components for some reason do proportional expansion on zoom and that just doesn't look or feel good. But this this feels perfect.
So we just installed that and let's go ahead and add it to our project ID layout. What we have to do is we have to import allotment from allotment and we now have to define some constants here. So minimum sidebar width, maximum sidebar width, default conversation sidebar width, and default main size. Then let's go ahead and wrap our children here in a div. Let's give this div a class name, flex1, flex and overflow hidden.
And then let's add allotment around the children itself. The allotment will have a class name of Flex1 and default sizes of default conversation sidebar width and default main size. So let me collapse them so it's easier to read. Like this. Which means that we're now going to have to add two allotment panes here.
So allotment. Oops. Allotment.Pane number one and Allotment.Pane number two around the children. Like this. Inside of this one, let's give it a snap property, a minimum size of minimum sidebar width, maximum size of maximum sidebar width, and preferred size of default conversation sidebar width.
And then in here I'm gonna add a div conversation sidebar. So let's go ahead and save this. And so far we're not really seeing much. Right? So if you take a look at how allotment should be used, we install allotment, we use allotment, but we also need to import its styles.
So let's make sure we add that. For now I'm going to add it here. Actually, I think we have to like do it at the end. There we go. And here it is.
Here is the split. You can see how it has this almost recognizable VS code like highlight. And it can also snap, I think, Maybe you can't. Let's see. Did I enable snap here?
I did, which should make it possible for us to snap this all the way back. But maybe I'm not understanding its property correctly. There we go. You can snap, which kind of means hide it completely and you still have an option to bring it back. So this is where our conversation sidebar will be.
And now we're going to implement this part right here in which we're simply going to implement, you know, the tabs. So where is this part? Well it's the children. So let's go ahead inside of... We don't have it yet so instead of app folder projects project id page this is where that is.
So let's improve that. So instead of rendering the project ID here, we're just going to render project ID view and pass it a prop project ID. Obviously, we have an error now because this doesn't exist. So let's create it back instead of features projects components. I will create a new file project dash ID dash view dot TSX.
I'm going to mark this as user client. And now I'm very simply going to prepare the following and let me end it here. There we go. So project ID view. It's gonna have one prop, project ID.
And that project ID will be the same type as in our project ID layout if you remember. ID projects. So let's import ID from convex generated data model and let's return a div project ID view. Great. Now let's go back inside of the project ID page and let's import project ID view.
And in order to fix the type error, we have to change this to be the ID projects as well. There we go. All fixed. So we should now have the conversation sidebar and the project ID on the other side. Going well.
Let's go ahead instead of project ID view here. And now what we're going to do is give this a class name, height full, flex and flex column, create a nav bar with a class name height of 35 pixels, flex, items center, background color of sidebar, and border bottom. And then let's develop a component called tab. This component will have some props such as label which is a type of string isActive which is a boolean and onClick which is a void, I mean a function. So label isActive and onClick.
Let's go ahead and return a div, a span inside and render the label. The span will have a class name of text small and the div will have an onclick property and a class name which will use the cnutil because this will be dynamic based on its active prop. So if is active it will have a BG background and text foreground. But if it is not, we're just going to have some default classes here. Such as flex, items center, gap to full height, px3, cursor pointer, text muted foreground, border, right, hover, bg, accent with a 30% opacity.
That's our tab component. Now that we have our tab component, let's go ahead and render it inside of here. So tab like this. Label, preview, is active, false, onclick, empty arrow function. Duplicate this and change this one actually the first one to be code.
And yes, in here we have a warning. So it looks like this can be written as height 8.75. Really cool. Let's write it as such then. And now as you can see, we have tabs, code and preview.
Clicking on them doesn't do much though. So let's improve that. In order to do that we have to introduce a state. So right here in the project id view, use state. Make sure to import this from React.
So I'm going to move this to the top. So what type can the state be? Well, only two things, either an editor or preview. And by default it's going to be editor. And the property that we are going to store is active view and set active view to control it.
So now we can modify is active and on click here to be like this and call the editor and the opposite in the preview. If active view is preview and set active view to preview. There we go. You can now switch between the two states. Perfect.
Now we have to add some content here. So outside of the nav here let's create a div with a class name flex1 and a relative and inside of here a div with a class name cn absolute inset zero if active view is editor it is visible otherwise invisible and a div editor there we go And then let's go ahead and do this again. So I'm just going to copy this. And we're going to do the opposite here. So if active view is preview render preview.
Let's try it out. So when I click on preview, I can see the preview content. And when I click on code, I can see the editor content. Great. So one thing left to do is to add a simple export to GitHub button, but without any functionality.
It's not even going to open anything. I just want to have the UI ready. So right below our last tab here, we're going to add a div. This div will have a class name flex1 flex justify end and height full. And then inside of here we're going to do the following.
So this will later actually be in a separate component, but for now we're gonna keep it here. I want you to copy the entire class name from the tab. And go ahead and add it here in a div. So class name and just paste the entire thing. The only thing we're gonna change is from border right to border left and gap to be 1.5 and then inside of here let's go ahead and render FA GitHub which we can import from react-icons forward slash fa and let's give it a class name of size 3.5 and then a span which will say export with a class name text small.
So let's go ahead and check it out. There we go. We now have a button to export to GitHub that currently does nothing but will in the future. Amazing! Exactly what we envisioned!
And more, I completely forgot about this indicator but we did it on the fly and it was quite easy for us to do so, wasn't it? So we are now ready to start doing some real work here, aren't we? Pretty good setup so far so we've created dynamic routing system we build resizable IDE split panes, we implemented navbar with project actions, we've set up editor and preview containers and we added the tab switching functionality. Awesome! Let's go ahead and merge all of that.
So this is chapter 8. I'm going to shut down all of my ideas here. I mean my terminals so get add dot get commit 0 8 IDE layout get checkout dash B 0 So git add .git commit 08 IDE layout git checkout-b 08 IDE layout git push-u origin 08 IDE layout And once we've pushed it, you should see it right here. And now we can go ahead and open a pull request. So I'm going to go ahead and open a pull request and let's review our code.
And here we have the summary. New features. We added individual project detail pages with dedicated layout. We enabled inline editing of the project names in the navbar. We introduced split-pane interface with sidebar navigation.
We added code and preview tabs for project viewing. We added a placeholder of project exporting and we are displaying project status indicators, import progress and save timestamps. Let's take a look at two important comments, actually three important comments that CodeRabbit left. It actually found some bugs which I've left. The first bug is instead of our layout.tsx it's a type mismatch.
Basically in here we define it as a string but CodeRabbit knows that instead of project ID layout we expect a type of convex ID of projects. We can actually confirm this bug. So if I go inside of app folder, projects, layout, you can see it's a bug, right? It's a super easy fix. We just have to modify this but I'm going to leave that for the next chapter.
I don't like to modify my branches once I push them. So that's definitely a thing to fix. Great catch by CodeRabbit. Then we have the second one. In here it mostly talks about how this is an incomplete implementation, which is true, right?
We have this unused project ID prop and it indicates that we should use this to fetch data. Correct, but this is still just a mock-up. We are later going to implement the file explorer so it will make sense. And here is an interesting one. So inside of the user name project it's telling us about a potential inconsistent ID usage in this optimistic update.
That is because we are kind of mixing the argument project ID and arguments dot project ID. You can see how here we're using project ID. But down here, at least somewhere I think, we're using arguments.projectid. Or it's just the fact that we even use this project ID because again I'm going to take a look just to confirm. So in my navbar when I implement rename project yeah you can see that I pass in .id.
So technically, why am I passing it here then? Right? We should probably get rid of that because it's just, It makes no sense. We should access this through arguments. This should be arguments.id.
I'm not going to do it now. I don't want to change my branch. But yes, CodeRabbit actually noticed an important mistake here. There's no reason for us to have to pass this because when you look at it, its only purpose is optimistic mutation for which we have a solution. We have the direct arguments which are more reliable than this because yes, if you pass one thing here and call another thing here there will be a mismatch with optimistic update.
Very very good catch by CodeRabbit. See this is what I was talking about. This is why I think it's important to have another set of eyes take a look at the code. Amazing, amazing job. We're going to merge this for now.
I like to fix these things in the second chapter rather than now so we don't mess the branch. And now that we have merged that, let's go ahead and go back to our main branch. Gitful origin main, so we are up to date. There we go. And as always, I like to confirm with my graph here.
There we go. We checked out for chapter 8 and we merged it back to main. Amazing, amazing job. I believe that marks the end of this chapter. We created this, this, this and this.
Amazing, amazing job!