In this chapter, we're going to build the projects page. This will include the Polaris branding, project creation and get feature, the actual project list, and some keyboard shortcuts as well as a command palette or dialogue. Basically, this is what it's going to look like. So this is the finished project. This is what we are supposed to have at the end of this chapter.
This is the Polaris branding that we are going to have. This is the new button and the only thing we aren't going to do is we aren't going to make this open a prompt simply because we don't have any functionality as to where to send that prompt at the moment. So we're just going to make this instantly create a new project and then we're going to modify it later. Same is for the import feature right here. But everything else we will be able to do, including this command dialog right here and the shortcuts.
All of that will work at the end of this chapter. So let's get right into it. The first thing we have to do is make sure our app is running. We will not be needing any ingest or anything like that so you can just have localhost 3000 running. And what I would like to do is just clean up our project first, right?
So let's go inside of source app folder and in here let's remove the sentry example page as we're not going to need it. And let's remove the demo page too. We are not going to need that either. And inside of the API, let's carefully go here. So let's remove demo, we don't need that.
And let's remove century example API. And inside of ingest route, we can leave it as is. We're going to clean up ingest later when we actually start building some real background jobs. So for now let's just head back to localhost 3000 where we have kind of started building the projects list. Right.
So what I want to do first is I want to go inside of convex and I want to go inside of schema here. The reason I want to do that is because I want to add all of the features here simply. So In case we are doing some UI changes depending on the field of the project in the database, we can do that and we don't have to fake it. So I'm going to go ahead and add export status here. Give it a V.optional like this.
And inside I'm just going to copy this V.union. Let's go ahead and add exporting. Let's go ahead and add completed. Let's add failed and let's add canceled. And let's also add export repo URL.
This will also be optional and if it is passed it has to be a string. So the reason I'm adding these is because I'm pretty sure our UI will look different if a project has this field. If it does we're going to show a little GitHub icon to indicate to the user like hey this was imported. Otherwise it will show a globe icon to indicate it's kind of on the cloud right. It's custom made.
Perhaps we won't need the export status for this exact chapter but still no worries. It's completely fine if we add it now. The import status is important so make sure you have that. And let's also add updated at field. So updated at is a number like this.
Great. So now let's go ahead and make sure we do npx convex dev simply so all of that is immediately synchronized. And there we go. Okay, so this is actually all successful. The problem is that we have existing projects, right?
So let's go inside of dashboard.convex.dev or you can use the link on the screen. Let's go inside of Polaris here. Data projects and let's just delete All of them. All of them are outdated. And let's do the same inside of tasks because we don't even have that anymore.
So we can get rid of tasks entirely. Let me see. There we go. You can delete the table from here. Perfect.
So we are only left with projects. That reminds me. We can also delete sample data.jsonl. So let's remove that. No need for that either.
Perfect. Now let's go ahead instead of convex and let's go inside of projects here. And now we're gonna go ahead and just fix the current create method. So let's go ahead and see the arguments here. Still, we're just going to use the name.
Nothing needed to change here. And then let's go ahead and do the following. So I'm going to do const project ID, await context database insert into project stable, name arguments.name, owner ID, identity.subject, and updated at will very simply be a date.now instance and let's return back project ID. All right. And now I want to do one thing.
We're going to use this a lot, like in almost every single query and mutation. So let's abstract it. So I'm going to go inside of convex here and I will create a new auth.ds. And I'm going to import mutation context from generated server and query context from generated server. And then I'm going to export const, verify out.
I'm going to make this an asynchronous method. And for the context type, I will use either a query context or a mutation context and then in here we can go ahead and return the identity. There we go. Now that we have verify out we can call this instead so in order to get identity we can just await verify out and pass in the context. So you can import that from .out.
Since this is a mutation instead of verify out, this is the type we will use. So if I didn't pass that, it looks like it doesn't have an error because they could be identical. Yes, it uses the generic mutation context. So maybe that covers the type for the other one. I don't know, but I kind of want to use this.
It works for me in the original source code. So I will teach you to do it here as well. Great. So this is our create method. And now let's go ahead and let's modify the get method here.
We are going to slightly modify it. So first things first we get the identity by using verify out and passing in the context here and then let's go ahead and store the result of this inside of a query and what we're going to do is we are going to add an argument here which will be optional limit v.optional and passing the number here so if we want to we can limit the number of results we get. So if arguments.limit Actually yeah it would be better to just not call items here at all. OK. Let's call this get partial like this.
And since the name indicates that it's going to be partial, we can actually make the limit the required because that makes more sense, right? If we have a separate query for making partial ones, let's go ahead and actually do that here and then we can very simply, instead of using collect, we can use take and just pass in the arguments.limit and I'm not sure I'm doing this correctly. There we go. Arguments.limit. Perfect.
And then in here well I think we can just return it now. There we go. So that is a get partial. And now let's just have a normal get, which will, well, we can copy it. It's going to be very very similar except it's not going to accept any arguments at all and instead of this it will just use collect and no need for this.
Looks like a weight has no effect on this type of expression. I'm assuming that's the case here too. Looks like it's not. Okay. You can see that it tells me a weight has no effect on this type of expression.
Okay. I'm going to trust it. So let's go ahead and have that. In here we got an error. Use query projects dot get.
So it looks like something is incorrect here inside of the convex one. So let me go ahead and do an x convex dev again in hopes that that will resolve it because I don't see any at least I can't see any issues with the code there we go convex functions ready so can I refresh this now okay I still cannot refresh it How about we go and take a look what it actually is? So source app folder page.tsx. Let's see what is the problem projects. What is the problem.
Map does not exist. Okay. API projects.get. Oh, I'm sure a lot of you have noticed this. Sorry about that.
Okay, if you didn't see what it was, I forgot to execute the collect method. Now it should work just fine. My apologies. Okay, so we now have the get method, we have the get partial so we can limit the amount of things we get. And we have a proper create method.
Perfect. So let's go ahead and let's start with the creation of this. So I'm going to go ahead instead of source features and in here I will create projects like this. Instead of projects I will create UI. My apologies, I'm not going to call it UI, I'm going to call it components.
And inside I will create, let's call this projectview.tsx. Like this. Let's mark it as use client export const projects view and let's return a div projects view just like that. And then I'm going to go ahead inside of the app folder page.tsx right here I'm going to go ahead and call this home I will remove every logic inside all the logic remove all of the imports no need for it to be used client anymore and I'm just going to return projects view. So that's going to be my only import here.
There we go. So now I will no longer develop here inside of the app folder. Instead I will just import projects view which is correctly put here in the projects folder so I don't have to care if you know this accidentally becomes part of the URL or something because that's how routing works inside of the app folder. I can safely develop things here in the structure that I want with the folder names that I prefer. Great.
So we are inside of the project view. Let's go ahead and let's start by actually building the layout. So I'm going to give this a minimum height of screen so it always takes 100% of the view. I'm going to give it a background color of sidebar, which is going to make it slightly darker. I'm going to give it flex, flex column, items center, justify center, padding of 6 and on medium padding of 16.
Let's go ahead and open a new div inside and in this one we're going to limit how wide the content will go. So maximum width of this content will be small. We use MX auto to equally push it from both sides and we repeat the flex call. We give it a gap for items center and let's go ahead and keep it at that. Now let's go ahead and let's add a new div.
So I'm going to add some spaces so it's easier for you to follow. There we go. Another div with the class name flex justify between gap four with full and items center inside. And then let's go ahead and add another div again I'm gonna add spaces there we go div flex items center gap do with full and group forward slash logo So this is how it's supposed to be. Okay.
Inside of here we're going to have an image element and for now let's go ahead and use if we have any image in our public folder so we can add an image later so we don't concern ourselves with that. Looks like we have our cell.svg. Can I use that? There we go. Perfect.
And let me give this an alt of polaris which will be the name of our app. And let's give it this a class name size-32 pixels in square brackets and the medium size 46 pixels in square brackets. There we go. So a very small one. And maybe I should zoom out actually so I see the browser mode here.
Okay. And then below the image or should I say next to it, we are going to have a text polaris, like this. And let's go ahead and add the Poppins font. So I'm going to go ahead and define that. Const font will be Poppins, from next font Google, subsets will be Latin.
And then we need to add the weight property so it's going to be 400 500 600 and 700. Great Now let's go ahead and let's prepare an import of, if you remember very early, CN. This helps us make dynamic tailwind classes. So we are using it for the first time now. CN, like this.
And in the first argument of the CN I'm just going to give it some default classes like text for Excel to make it larger on medium text 5 Excel font semi bold text white even though that's already presence and I need to do that. And then I'm going to go ahead and add a comma here and I'm going to pass font dot class name and that's going to change the font. Perfect. So now let's go ahead and let's go outside of this div, outside of this div. And in here let's go ahead and open a new one.
Like this. This one will have a class name of flex flex call gap 4 and full width. And then in here we're going to create a grid. So class name grid grid columns 2 and gap 2. Let me just fix the indentation here and inside we're going to use our chat CN buttons so make sure you add an import for that and I'm going to give this a couple of things so variant here will be outline On click for now is just going to be an empty arrow function.
Class name will be full height, items start, justify, start. Padding of 4, BG of background. Border. Flex. Flex column.
Gap six and rounded none. So we make it sharp inside of here. Open up a div and inside a sparkle icon from Lucid React. So make sure you import sparkle icon and I'm just going to move it up here. OK, I'm going to fix this little issue I've created.
The sparkle icon will have a class name of size 4, like this, and the div encapsulating the sparkle icon will have a class name of flex items center and justify between as well as a full width. Then below this we're going to add our shortcut UI so KBD component. We have this from chat CN UI so KBD and let's go ahead and add a class name here BG accent and a border. Let's make sure it's closed and then inside of here we're simply going to say what our shortcut will be. So for me it's going to be command J since I am on a Mac OS.
You can also use you know control plus J whatever you want right. If you want this specific icon I would suggest just googling command icon copy paste you know just find it on Google okay and then outside of this div I'm just going to add a new div and this span which says new and I'm going to give it a class name of text small like this. So let me go ahead and check it out. There we go. Looks nice.
We now have this button right here. Perfect. Let's go ahead and let's copy this button. So the entire thing we can duplicate it below. There we go.
Now I have two of them. So I'm going to work in the bottom one. The class will be exactly the same. The shortcut will be the letter I. Instead of new, it will say import.
And for the icon, we actually have to install react icons for this one so let's do that. And install react dash icons simply because we'll see the react doesn't really have a good github icon so make sure you have react-icons installed and then let's go ahead and import fa github from react-icons forward slash fa And then we can replace down here this one find import and find sparkle icon and replace it with github. There we go. We now have our new and our import buttons right here. Amazing!
So now what we ought to build next is the project list. So where does the project list belong? Well it actually belongs right outside of this div right here after we close the button. So let's prepare it projects list like this. Now let's go ahead and create it.
So instead of components here, I'm going to create a new file projects dash list dot T S X. And let's start creating that. So first things first, I'm going to add an interface projects list props, which will have one prop on the view all function. And then we can go ahead and create a proper export projects list on view all. Don't worry, I'm going to collapse this so you can see better.
There we go. Very simple. So projects list is a component. In which we've destructured on view all and we gave it a type of project list props above so make sure you have destructured the props here this can also be written as props like this but we have this structure it since it's gonna be the only one here. Alright so now that we have that I'm gonna go ahead and write return I'm gonna add a div here with a class name flex flex column and the gap of four like so.
And then let's go ahead and let's actually, you know, list our projects. And before we do that, let's just render them. So projects list, make sure you import it. There we go. I'm going to give it an on view all to be an empty arrow function simply so we get rid of the type error.
And now what I like to do is I like to abstract my hooks. So I'm going to go inside of features Projects and I will create a new folder called Hooks like this. And my hooks will have a file called use-projects.ts and in here I'm going to import, you know, use query from convex react, import API from convex generated API, and I will export const use projects here like this and return use query API projects get like this. And then I'm going to immediately duplicate this and I will call this use projects partial with a limit which is a type of number. Get partial.
And in here we have to pass the limit. There we go. So simple abstraction so I can now call this instead. So let's do that. Let's get set of projects list right here.
And let's do that right here. Const projects use projects partial from hooks use projects and pass in the number. So I'm going to do six actually so six of my latest projects and if projects is undefined, that means it is still loading, so I'm just going to return a spinner component. You can import this from components UI spinner and I'm going to give it a class name size4 and text ring so if you refresh for a brief second you see the spinner in fact if I change this to true You can see how it looks like. Let me zoom in so I can actually see what we're developing.
There we go. So yes, in convex, if the query result, any query result is undefined, that means it's loading. Because it's either going to be an empty array or null if we weren't able to find it. Right, so if it's undefined that means it's loading and that is from the documentation of convex right so that's how you detect if something from convex is loading. Okay now inside of here let's go ahead and do something.
So I'm gonna go ahead and do most recent projects and then I'm gonna do rest from projects. So what did I do here? Well, I will extract the most recent project I have worked on from the rest simply because I'm gonna have a separate card showing me like hey you can continue working on your most recent project and then here are the rest of the projects. So that's why I'm doing this. For now, let's just do the rest thing.
So if rest.length is larger than 0, meaning, okay, we do have the rest of the projects, in that case, let's go ahead and start creating a list for this. So let's give this flex flex call and a gap of two. Then let's go ahead and create another div inside with flex items center and justify between and gap to then let's go ahead and create a label. So basically a span element with text extra small and text muted foreground which simply says recent projects right. OK.
So the reason we are not seeing that text would indicate that this is actually empty So let me go ahead instead of use projects specifically get partial here. Let's see Can oh yes, it is because we don't really have any projects here so this is what I would like to do right now I would like to just... Just because it makes no sense you know to develop thing a UI thing we don't see right So let's just quickly go back to the projects view right here. Let's focus on that and let's prepare our hooks useProjects. And inside of here let's go ahead and let's create export const use create project like so return use mutation from convex react api projects create like this That's the only thing we're going to do now.
So let's go back inside of the projects view and now I'm just going to go ahead and I'm just going to add that hook here. I'm just gonna add that hook here const createProject useProjects useCreateProject from .hooks useProjects. Perfect So now let's go ahead and find this new button, right? And in here, I'm going to go ahead and call create projects and for the name. So what should we use for the name?
Well, the solution that I have found is to basically use a library, which will allow us to create a unique slug. So let's go ahead and do that. The package name is the following. So unique names generator. So let's do NPM install unique dash names dash generator.
This one. Perfect. And let's go ahead and import everything we need from it. So we will later, you know, move this somewhere else. But for now, it's just going to be here.
So let's import adjectives, animals, colors and unique names generator itself from the same named package. And then once we have that we are able to actually generate the name. So I'm going to go ahead and prepare that just above here in the on click function. So the project name will call the unique names generator function and instead of its object, we're going to add dictionaries. So we're going to use adjectives, animals and colors.
I simply found that to be a nice combination. The separator is going to be a dash and we're going to use three words. This is kind of the standard in most of these types of projects you see these days. So there we go. Now when I click on new here, I'm not sure if it works or not.
Let's see here. It works! Do I have the fun name? I do. Shaky Canid Teal.
So if I click again here, there we go. It works. And you can see my owner ID is stored inside and now I can even see the recent projects here. Perfect. So make sure you have at least two created.
That's why the recent projects were not showing. Remember inside of projects list we separate the most recent one from the rest. So make sure you have at minimum two items inside. So maybe for simplicity sake, we can just use projects for now here. Projects.
That will be simpler, I think. Great. So now that we have the span recent projects, let's go ahead and add a native HTML button with view all text here and a KBD. So import it again. Let me just move it to its place here.
Okay. KBD. And inside I'm just going to do, well I'm just going to write the shortcut which will be the letter K and the command. I'm going to give this a class name BG accent and the border. There we go.
And for the button itself it will have a couple of class names. So let me go ahead and add them here so you can see just a second. Flex items center gap to text muted foreground text extra small on hover text foreground and transition colors. There we go. So yeah this is how it's going to look like.
All right. Now let's go ahead and let's actually render the items. So outside of this div, add an unordered list element, ul. And let's do projects.map get the individual project here and I'm going to render project item here Let's give the unordered list a class name flex flex column and for the project item itself let's pass in a key of project underscore ID and then data project. And now let's go ahead and let's actually create the project item.
And we're gonna do this in the very same file, right? So it's easier. So I'm gonna do this below here or maybe we should do it above so we don't reference it before it's created. Project item will accept data and the data type will be a type of document. We can import doc.
Doc not from Zod from convex. So let me go ahead and find convex generated data model. Is it doc. I think it is doc. And then we have to define from projects.
OK I think I'm not importing this correctly. The doc is used but it's never read. I think I just have to finish the function. There we go. Yes.
So make sure that we are using the data here. We gave it a proper type. And now I'm going to go ahead and use a link component from next link. So I'm going to give this an href of forward slash projects and then data underscore ID Like that. Let's go ahead and give the link a class name.
So I like to have my attributes one below another like so. So it is going to have a couple of class names as well. Let me go ahead and add them here. Text small, text foreground with a 60% opacity, font medium on hover, text foreground, padding on the y-axis 1, flex, items center, justify between, full width and it's also going to be a group. So now inside of here I'm going to open a div with a class name flex items center and gap to let me fix my typo here And inside of here we have to render the icon of this project.
So I will now use a globe icon here as default and then I'm going to change it later. So make sure you just import Lucid React here, the globe icon. So that's the first thing we're going to have. And then I will add an arrow right icon again from Lucid React. Make sure you import that.
So outside of this div. And then let me see if I... My apologies no, not yet we're not gonna have this instead let's just add a span and in here we would have to format the timestamp and for that we're going to use date FNS. So let's npm install date FNS. So we have to now import Format distance to now and let's go ahead and create a little helper function here.
So I'm just going to add that here at the top. Format timestamp will accept the timestamp which is a type of number and it will return format distance to now like this and inside it's going to pass the new date around that timestamp And then as the second argument options in which we add suffix to be true. Great. Now that we have format timestamp inside of this span, let's go ahead and simply use it for project. It's called data.
Data dot updated at. There we go. Now let's go ahead and render the project item here. Project item cannot be used as a JSX probably because I never return anything here. So make sure to return something.
And there we go. So I can already see two of them here. Perfect. Now let's go ahead and just style this a bit. So what do we have to style?
First things first about this span here. Oh, we are missing another span, which renders the data name. There we go. And let's give this a class name of truncate. Like so.
For this span right here, it will have some larger class name. Text extra small, text muted foreground on group hover, text foreground with 60% opacity and transition colors. All right. So we have that And now let's go ahead and check it out. Let's see how it looks like.
There we go. You can see how I create a new one. Perfect. So what should we do next? The new one is finished here.
Okay, so the globe is too big and the globe actually shouldn't be rendered like this. Instead, we should develop a function called getProjectIcon and we're going to accept project as the prop and if project import status is completed we will return fa-github icon So make sure to import this from react-icons.fa And let's also give it a class name Size 3.5 and text muted foreground And then let's go ahead and just do other options, right? So if import status is failed, let's import alert circle icon again from Lucid React. Then let's go ahead and check if import status is importing. And let's use the loader2 icon.
So all of them have the same class name except the last one which also has animate spin. So just make sure to add that. And then let's go ahead and add the default one. So outside of all if clauses, let's return the globe icon. You can obviously turn this to a switch case if you prefer it that way.
For some reason I have an aversion towards switch cases. I just don't like them. But you can of course use them if you prefer. And then replace the globe icon with get project icon and pass in data. And all of ours will have the globe icon, right?
But if you want to play around, if you go to one of your projects here, I don't know, import status, if I say completed in quotes and save, you can see that one of them was changed to GitHub. If I change another one to failed, you can see it shows an alert. And if I change another one to importing, It's a spinner. So there we go. Those are all the instances that can happen.
Great. Now let's go ahead and show the last project we worked on into a separate container simply so we have some better user experience here. So I'm just going to copy this because it's identical as what we're going to build. And it's going to be called a continue card like this. So it will have the exact same props here.
And in order to make it immediately visible, let's go back inside of projects list here and then let's go ahead and do the thing with most recent and the rest. So now let's use the rest.length and let's use rest.map which will as you can see remove one and then what we can do is just do continue card and pass in the project or let's do data most recent. Great Now let's go ahead and develop the continue card. Inside of here let's return a div with a class name flex, flex call and gap2. Then inside of it let's add a label span with a text last updated and text extra small text muted foreground and that's it.
Then let's add a button but this time a button from components UI button. There we go. And let's go ahead and give it a few props. So it's going to have a variant of outline. It's going to have an as child prop and it's going to have the following class name.
HeightAuto, ItemsStart, JustifyStart, Padding4, BG, background, border, rounded none, flex, flex column, and gap 2. Then let's render a link. Let's give this an href, forward slash projects, and then render the data underscore ID here. And the only class name the link will have is group. Then let's go ahead and render a div inside with a flex-items-center, justify-between and with full class name.
Then let's go ahead and open a new div inside flex-item-center and gap-2 and then inside of here we're going to render get project icon for that specific project we are continuing our work on. And next to it we're going to add a span with font, medium and truncate class name to render the name. And then outside of this div let's render an arrow right icon from Lucid React with a class name Size 4, Text Muted Foreground, Group Hover, Translate X 0.5 and transition transform. There we go. Like this.
Now let's go ahead and render the format timestamp as well. So outside of this div text extra small text muted foreground And let's render the timestamp. There we go. So now this is our kind of last worked on project. So we can access it easily here so we don't have to search it through here.
Excellent. So this is now working pretty good. What I want to test is what happens when I have no projects. That's what happens. So let's go ahead and make sure that inside of projects list only if most recent exists we show that otherwise we don't render anything.
There we go. So let's click new here and there we go. That's how it looks like. And now I want to show you something that we can do to improve our hook actually. So instead of use projects if you find use create project You can see there's kind of a slight delay happening, right?
And during that delay, we don't even block this. So what we can actually do is we can add a optimistic mutation. So with optimistic update like this and let's just push a new project to the list sooner. So let's open local store and the arguments and then let's go ahead and let's get all the existing projects. So existing projects using local store get query API projects get if existing projects is not undefined meaning they have loaded.
Let's go ahead and kind of simulate a new project. So date.now, const new project will be an object, underscore ID will use crypto.random underscore ID will use crypto dot random UUID and we have to cast that as the ID from convex. So make sure you add this and let's also add doc here. Besides ID we will have creation time which we'll use now, name which we'll use arguments.name owner ID which will be anonymous updated at which will be now. We could also use user ID, Use out from clerk next JS and then we could pass user ID here I Think that should work, too So you can import user use out here.
So we don't pass anonymous And then what we have to do with this new project is call the local store and set the query API projects get skip the arguments part and then inside of here let's go ahead and add new project to the list of existing projects. And let me just see what type problems do we have here. So we have date.now and this can actually be fixed with a comment actually. Cannot call impure function. Let's just go ahead and add slint disable React Hooks purity.
So that should get rid of that. This is what I've added. And now I'm trying to figure out. Ah, I see. So let's just use anonymous.
It doesn't matter either way. This is not a back end anyway. So now we have optimistic update and if I'm correct this should now appear instantly and I think it does. It's like significantly faster. Let me go ahead and delete all of them again so we can test it out.
So now we don't even need like any loading state because it's instant like it's like tuck tuck. It's super fast. Great. So yes you can do optimistic updates with convex as well. So I believe there's one thing left to do here and that is to implement the view all and the, actually it makes no sense to implement shortcuts because shortcuts will open dialogues.
So the only one that makes sense is the view all one. So we're going to we're going to create a project command dialogue. So inside of components new file projects dash command dash dialog dot TSX let's market this use client and let's go ahead and import everything we are going to need. So okay we actually don't need use client because it's already within a client component. So use router, FA GitHub, the following icons from Lucid React, alert circle icon, globe icon, loader to icon.
Then we're going to need all of these from components UI command. So dialog, empty, group, input, item, and list. All of this come from Chatsy and UI. And last but not least, use projects from hooks use projects. Then let's create an interface projects command dialog props to accept a boolean called open and a function on open change which will be used to modify that boolean.
Then let's go ahead and export the following function projects command dialogue using our projects command dialogue props and destructuring the props above. Perfect. Let's define the router here. Let's define projects. Let's quickly define handleSelect method.
So whenever we select one of the projects from this dialog we will get project ID from that selection. And we can use the router.push method to redirect to projects project ID. Like that. And then let's call onOpenChange false so we immediately close this dialog. Great.
Just make sure you have imported useRouter from next navigation because in the previous versions it had another one. So now I'm going to go inside of projects list. I'm going to find this function get project icon and I'm going to copy it. So the entire function. And I'm going to go ahead and let's see.
I'm going to add it up here. I will see if this ends up being identical, we can export it. But for now, we're just reusing it in two places. And yeah, let's just do doc here. We have to import this from convex generated data model.
So make sure you have this import. We now have get project icon. Perfect. And OK, the only difference is going to be the size of the icon. So instead of 3.5, we're going to use 4.
It's just a subtle difference, but it makes it look a little bit better for the dialogue right and Now inside of here let's return and then let's go ahead and render everything we need, right? We need the command dialog. The command dialog should have open and on open change which we can immediately pass along. Let's give it a title and description relevant to what they do. Search projects.
Search and navigate to your project. Let's render a command input with a placeholder, search projects. And then let's go ahead and render the command list. Let's go ahead and define the empty state which will simply show the label no projects found. Then let's go ahead and add a command group with a heading of projects.
And then inside of here let's iterate over our projects with a question mark in case they are empty and in here we are going to render the command item and looks like I'm doing something incorrect here let me just figure out what. There we go. I was missing another parentheses here, right? So I have to add it. So let's give the command item a key of project underscore ID.
Let's give it a value of project name dash project ID. The reason we are doing this is because if you just use, you need to use the project name because that is what will be highlighted when you search. So it's going to look for value. The problem comes when you have projects with the same name and then all of them are highlighted. So that's why we kind of combine it with project ID.
So they are kind of unique in their own way. And let's add onSelect to call our handleSelect method and redirect to the project ID. Then let's go ahead and render the getProject icon and finally the project name. Now we have to go back to the project's view page and let's add a state. Command dialog open, set command dialog open, controlled through use state.
I'm just going to move this to the top. And now that we have this we can go ahead and render it in the return. So I'm going to do that within this fragment. So I'm going to encapsulate the entire content of the return inside of a fragment. So my order is semantically correct because the project's command dialog is above all of those other elements, right?
So make sure to import project's command dialog, which we have just developed here, right? Make sure you have exported it correctly. And I'm gonna go ahead and give it open and on open change. There we go. And now what we have to do is we have to pass the on view all option properly.
So find project list. And passing the on view all to call set command dialog open and set it to true. Now I just have to confirm that I'm using on view all within the projects list. Looks like I'm not. So I'm just gonna go ahead and find my recent projects here and then here we go.
This button right here will call on click on view all. Let's test it out. So I'm gonna go ahead and click view all and here we go. So let's search for Cougar. There we go.
Perfect. And clicking on it will redirect me to a 404 because well, we haven't developed that yet. So what's left to do now is just add a shortcut. So let's go inside of projects view back here and using use effect we can very simply achieve this effect. So use effect Make sure you have imported it from react, same place as useState.
Const handleKeyDown event keyboardEvent if e.metaKey or e.controlKey is included and then if e.key is equal to k, e.prevent default, set command dialog open to true. As simple as that. Let's add document add event listener on key down and passing the handle key down and super important make sure to unmount it so document remove event listener on key down handle key down and let's move the state above the use effect like this so it can properly access it. So if you try pressing on the control key or the command key if you have macOS and hold the letter K at the same time, you can see you can now open view all. Amazing!
And to end the chapter I think the only interesting thing to do is add a real logo to our app. So I found a logo on Untitled UI And usually I also use Logo Ipsum. So whichever of those you like, go ahead and just click copy, you know, and that will copy the SVG. I think it works very similarly here. There we go.
Yeah. So Untitled UI and Logo Ipsum are my go-to's for finding logos or if you want the exact one that I have using the link on the screen you can visit my assets, find logo.svg. I'm going to copy raw file and then I'm just going to go ahead inside of source, my apologies inside of public and I will create logo.svg I will confirm this open using standard editor and I will paste inside. There we go. That's how I do it.
You can drag and drop if you want. There we go. This is the logo. So let's go inside of page.tsx, projects view and let's remove the Vercel one and use our logo.svg and yeah this is a warning that we should use next image instead of image if you want to you can just turn that off let me use for this line. No.
Not working. OK. Yeah. It's just going to show like this. It's OK.
It's just a warning. And that's it. I believe this is exactly what we've envisioned. Obviously we don't have the dialogues but those will come and yeah everything else works just as it should. Let me see I think the only thing we might want to improve could be instead of convex projects maybe the way, maybe the order of fetching these so we show the newest ones first.
Let me see. Yes, we can do that. So in .get, let's go ahead and do .order descending. And in .get partial, let's do the same thing. Descending.
Let me refresh and that should reverse it now. Yes, because when you create the newest one, that one should become the last updated one. There we go. So now they are constantly switching. You can see how fast this creation is.
That's optimistic mutation. Amazing, amazing job. I'm super satisfied with how this went. So let's go ahead and review all of these changes. There's a lot of them, right?
So I'm going to go ahead and do git add and then a dot git commit this is chapter 7 so 0 7 projects there we go and then git checkout dash b 0 7 dash projects I'm I keep checking if I'm correct with my names now because of that number I missed so git push origin 07-projects there we go perfect So we have officially pushed that branch and now let's go ahead and review it. So I'm going to open a pull request and this time we can definitely let CodeRabbit review all of these changes we just did. So in this pull request, we added Projects interface with search functionality using the command dialog. We added Project Creation, which now generates random project names automatically. Recent projects overview displays the most updated projects and we also have added the export status for tracking projects.
If you remember I added this because I thought we might use it for the UI. It looks like we only use the import status but still it's not going to hurt. We remove the demo and the example pages and we added new dependencies and looks like our code was very good. No comments from CodeRabbit besides some nitpick comments which are also interesting as you can see exactly what we said. We could potentially extract get project icon to avoid duplication and in the projects view it is detecting that we are missing a keyboard shortcut which is very interesting because the only thing it knows is that one of them is implemented right and it only knows that in the UI we show the other one.
:15 Very very interesting but yeah we will add that later when we do. Again, consider using the Next.js image component so same as the linter. We do have some buttons with no functionality like the import button, we will add that later. In here we added SLint disable for the date.now so it suggests explaining why we did that. So you can see it even agrees with us.
:42 It just wants us to explain why. Overall, very very good. Yes, we can definitely you know validate the limit parameter but yes, most of these are just, you know, nitpick comments, but still very cool to have someone else take a look at the code before we merge simply so we know if we made any serious mistakes. Amazing. So that is a branch 07 project.
:07 Now let's check out the back to our main branch and let me go ahead and just try it like this. There we go. Git checkout. My apologies. Git pull origin main.
:20 This should now synchronize our local state with what we just merged. And as always, I like to confirm this by going inside of my source control, opening the graph below, and here we have it. 07 projects detached into its own branch and then merged back to main. I believe that marks the end of this chapter. We built the landing page with Polaris branding, we built the project creation and get feature, project list layout, and finally keyboard shortcuts and the command dialog.
:53 Amazing amazing job and see you in the next chapter.