So before we get into building the design or the templates section, I want to go back to the home page here and I want to create a section here which will give us greater control over all of our existing projects. So basically We are now going to create the API routes so that we can display all of our projects here, keep them updated, duplicate them and also delete them. Let's head back inside of source, app folder, API, route, projects. And inside of here, let's go ahead to the top and let's create a get route so this will simply fetch all of our templates so let me go ahead and not templates yet projects for now So let's go ahead and make sure we are doing verify out here and for the z validator here we're going to send a query and in the query we're going to send a page which is a type of z.course.number And same is true for limit. And now let's add an asynchronous controller here, which has info about the context in the params.
Let's get the auth from c.getauthuser, not authconfig, authuser. And let's get the page and limit from cRequestValidQuery. If we are missing the auth token ID, in that case, let's also put a question mark here, as usual we return back an error and a status code of 401. Now let's go ahead and let's fetch all of our projects. So we select everything from projects where equals projects user ID with our auth token ID.
So we don't need multiple where queries here, only this one to ensure that we are loading this user's projects. And then let's limit them by our limit variable and let's offset by creating a page system so page minus one times the limit. Right so we don't have an option for page so we have to convert the logic of page by using the offset right here. And let's order by descending which you have to import from Drizzle ORM so make sure you've added descending. We're going to import projects by updated at like this.
Great! And now let's go ahead and let's return c.json data which holds our projects And also since we're gonna have pagination, we have to return the info about what is the next page. So if data.length is equal to limit, in that case the next page plus one is the next page. Otherwise we have no next page and we have reached the end of our pagination, right? And now let's actually create our query for this.
So for this we're going to use useInfiniteQuery. So it's going to be a little bit different. So let's go inside of source features and let's go inside of projects here and let's copy the useGetProject and let's rename this to useGetProjects so multiple. Let's rename this hook name so useGetProjects like this and let's change this response type which I'm not even sure if we are going to need or not but let's change this to simply be projects get so let me zoom out this is how it should look like. In the response type we should get data and next page which is either a number or null.
And inside of use get projects we no longer needed the id here and we don't need this enabled and the query key will just be projects like this. And we are no longer accepting anything inside of here. Well, we are. So let's go ahead and remove the ID prefix here. And this is what we're gonna do instead.
We're gonna add a query. And in the query, we have to pass in page. Let's make that one and limit let's make that five and since yeah basically you cannot exactly define a type on a query So through URLs and through network requests, queries have to be strings, right? So you have to mark them like this on the client side, but on the server, We can work with them as if they are numbers because we used this kind of course type here. So if you removed this type, I believe then you would get some incompatible types later.
So that's why we added this type. So let's go ahead and write it like this and now let's do the following. Let's change this query to not use useQuery but instead useInfiniteQuery. So make sure you've imported useInfiniteQuery because we're going to have pagination here and for the useInfiniteQuery we have to define the response type ourselves which we have written here above so make sure you also look for 200 inside of it and for the error it's going to be error like this and now let's go ahead and let's add the initial page param which will be 1 and in the query function here we are now going to get the page param and how do we define what is the page param by adding a method get the next page param so from the last page or the current page we're going to choose next page and now that is what will be transferred inside of this props right here so that in the next query function we can call our well next page great so we have this and now let's go ahead and see what else we have to modify here what we have to modify to get rid of the error is to not return just data, right?
Or we can... This is what's best for us to do simply return response JSON there we go, so you can see that has correct types because then in the next iteration we're gonna get the next page here. And now let's go ahead and put this page param to use. So the limit can stay 5 but the page has to be controlled. So we could just put page param but unfortunately the types for this are not exactly great.
And I am half, you know, I'm uncertain, half-half. There might be something which I have configured wrong but I've also searched for this exact query and for the types and I found out that as at least how I understood it is that they've done some compromises because it's simply impossible to satisfy this kind of a complex query. So because of this, the page param has an unknown type. So let's go ahead and define it as a number and let's modify it to string. Like this.
Because in the backend we are returning a number. Next page is a number. But inside of here, remember, it accepts string because query from an HTTP request has to be a string. So this is how you write it. We have to kind of push it manually here, right?
But overall I think it's pretty good. And let's say failed to fetch projects here. And now that we have that, let's go ahead and let's actually render all of our projects in this space right here and for that we're gonna be using a table component. So let's go inside of our terminal and let's do bunx-chat-cn-ui add latest, add table Great. Let's do bun run dev again.
I think that's the only component that we are going to need. And now let's head back inside of source app dashboard page. DSX right here and below the banner we're now going to add a projects section like this so let's go inside of the dashboard and let's create projects-section.tsx let's mark this as use client let's export const projects section let's return a div projects like that Let's go back inside of the page and we can now import our new projects section. And we should just see a text here which says projects right after I refresh and this reloads. There we go.
Now it says projects. Great. So let's go ahead and let's actually... Well, let's code this, right? So inside of this div, we're gonna give the div first of all a class name, space Y4 and then in here let's go ahead and render a text which can say recent projects, or it can be a paragraph as well.
And let's give it a class name, font, semi bold and text large or we can actually use a heading maybe heading 3 perhaps there we go, recent projects, great and now below that we're gonna go ahead and render our table. And we can import table not from Lucid React, that's not where I want to import it from. So let's go ahead and import table from components UI table, like this. And besides table, we're also going to need the table row table body and table cell. There we go.
So now let's close the table here so that we can now add the table body like this. So right now I don't think anything will be visible here at the moment. So let's go ahead and let's actually add our use pages, use projects query here. So I'm gonna go ahead and add use get projects. So make sure you use the multiple, right?
From features projects, API use get projects. And now inside of here, we're gonna get a couple of new constants so data status fetch next page method is fetching next page and has next page so we get everything we need for infinite loading right great and now that we have the data here what we can do is the following so if we don't have data dot pages dot length like this we can go ahead and return a div with the class name space y4 so basically very similar to what we have here we can copy the h3 heading inside and we can add a div here with the class name flex, flex column, gap y4, items center justify center and height of 32 And then inside we can add a search icon from Lucid React. We can give the icon a size 6 and text muted foreground. And we can add a paragraph no projects found. And give the paragraph a class name text muted foreground and text small so to try it out let's go ahead and let's turn this into true like this just so we can see there we go so this is how it's gonna look like no projects found great And now let's remove this hard-coded true here and let's now create an error state.
So if status is error like this Let's copy and paste this return from here. We're gonna leave the label as is. We're gonna change the icon to be alert triangle from Lucid React. So just make sure you've added the import for it. I'm just gonna move this to global imports above and we're going to write failed to load projects.
So let's check that out by adding true. There we go, failed to load projects. Great. And then we're gonna have if status is pending. And let's copy and paste this again like this but we're not gonna have a paragraph we're just gonna have a loader again from Lucid React and we're gonna give this an animate spin value so now even if you refresh you should see a little spinner here for a second.
Perfect. So let me just check. Do I still need to have this here? Yeah, it looks like I don't. So it initially warned me that I need to put a question mark here but it looks like that if we handle an early return by checking the status of error and the status of pending looks like it properly infers that at that point we are already going to have the data.
Let me just confirm if I console log this there we go now you can see that there is a potential error here but if I handle the error by with an early return then I can safely directly access data pages.length for example. Great and now let's finally iterate over our items inside of here. So inside of the table.body, we're gonna go ahead and go over data.pages.map. And now we get the group from this page that we have like this. And let's now add a react.fragment.
And we have to import react here so import react from react and for the react fragment now we are only going to use it so that we give something a key. So we're going to simply use the I or the index for the key. And now inside of here we're going to iterate directly over our group which we now have. So group.data.map and now this will be our project like this. So let's go ahead and add a table row.
The table row will have a key of project.id and then we're going to have some table cells here. So this table cell will have a file icon from Lucid React so make sure you've added this import here. Let's give the file icon itself a class name of size 6 and let's render the project name inside of the table cell. Let's go ahead and modify the table cell by giving it a class name font medium flex let me scroll up so the tooltip doesn't hide my text, items center, gap x of 2 and cursor pointer like this. So now if I expand this a bit there we go we have the start of our table right here and this now looks clickable that's because that part will be clickable so we're gonna do the following let's also add the router here so const router useRouter from next navigation I'm just gonna move the import to the top here.
So here it is from next navigation. Now we have the router constant. And now what we can do here is if we click on this part of the table cell we can do an on click, router.push and then we can go to slash editor directly to project.id so we could technically wrap the entire table row in a link but a it messes up with the styles and B. It will kind of conflict with the drop down we are planning to add so that's why I chose this option instead. We are going to have specific table cells which can be clicked which will redirect to the editor and that specific project.
So already if I go ahead and click start creating here right now we don't re-invalidate anything here so nothing is happening so we're gonna attempt to refresh ourselves. So let's just wait for me to get redirected to my editor it's taking some time here let me just there we go it's finally happening let me go back here let me refresh and there we go you can see how now I have two elements inside of here So let's go ahead and finish coding this. So just make sure you refresh if you've created one and you can't see it. That's normal. So just refresh.
And now let's go ahead and let's add another table cell. Now this table cell will simply tell the user the project width times the project height and the format or the value which is I'm sorry the units which are pixels. Great and this table cell itself will also have a class name and that class name is that it's going to be hidden on the mobile devices and then on medium it will display the table cell. It's also going to have a cursor pointer when visible because we're also going to share the onClick method for it here. There we go.
So not visible on mobile but on desktop it is visible. Great. Now that we have this let's go ahead and copy another table cell here. And now we also need a package here. Bun add date fns bun run dev again, let me refresh my project here and we are now going to need format distance to now from date fns so format distance to now from date fns like this and let's use it in our newly copied table cell so I copied this one from above and now in here we're gonna do the following we're gonna call format distance to now using project created at, sorry, updated at, because we want to see the newest info, right?
When was it last modified? And let's add suffix to be true. And yeah, this one will also be hidden on mobile. There we go. So now we have two minutes ago and 25 minutes ago.
Perfect. And now what I want to do is I want to add the last table cell which will also be a drop-down menu. So let's add this table cell here. This table cell will have the following class name. FlexItemsCenter and JustifyEnd.
And now let's go ahead and let's import everything we need from the drop-down menu. So let me go to the top here and let's add the drop-down menu content drop-down menu drop-down menu item and drop-down menu trigger from components UI drop-down menu Let's go to our newly copied table cell. Let's mark everything in drop down menu inside. Let's give this prop model false because this drop down menu will open a confirmation model for when we want to delete this so we want to make sure that it doesn't break our entire page. So that's why we are adding this prop.
Now let's add a drop-down menu trigger. Let's give it an as-child property, so it will become its child, which in our case will be our button, which we can import from components UI button. Let's go ahead and simply add a more horizontal icon from Lucid React here. Make sure you add this import. I'm just gonna collapse all of my elements because I'm gonna add more of them so that they are more readable when I go up here and show you.
So we just added the more horizontal icon. Let's give it a class name of Size4. Let's give our button a size of icon, a variant of ghost, And disabled for now will be false. We're later going to add it. Great.
And let me just see why exactly is it behaving like this? So this is fine, yes, but on mobile... Oh, it is fine as well. Okay, so it's only on this extremely small screens where something weird happens. Okay, everything is fine.
Great, and now outside of the drop-down menu trigger, we're gonna have the drop-down menu content. Let's give this a line of end and a class name of width 60. Now inside of here let's go ahead and let's add the drop-down menu item. This drop-down menu item will simply say make a copy and it will have a copy icon from Lucid React so make sure you add your copy icon here. Let's give the copy icon a class name of size 4 and margin right of 2 and let's give the drop down menu item a class name height of 10 and cursor pointer.
Let's give it a disabled of false and let's give it an on click of an empty array for now. Great. Now let's go ahead and let's copy this drop down menu item and let's go ahead and for this one let's write delete And let's go ahead and add a trash icon again from Lucid React. Make sure you've added that. Great.
So let me just go ahead and take a look here. Now I'm going to refresh this and now I have a make a copy and delete here. Great! So what I want to do now is add the fetch more button if it exists. So let's go outside of our table component right here and we're going to leverage our as next page to render a div which will encapsulate a button.
But let's give this div a class name of full-width flex-items-center-justify-center and padding-top of 4. And inside let's add a button component which will say load more. We're gonna give this button a variant of ghost and an on click which will very simply fetch next page and disabled if is fetching next page like this great so one thing I want us to do so we can test this in a nicer way is to go inside of the following hook features projects use create project and in here we have to do invalidate the projects query so let's import from tanstech react query use query client Inside of use create project let's get the query client, use query client like this. And then in the on success below the toast let's call the query client dot invalidate queries by query key which will be projects and just confirm that inside of your new use get projects, that is the query key you're using. Make sure you didn't add a typo inside of here.
So now every time you create a new project, this will get refetched. Let's go ahead and try it out. So I'm going to refresh everything here. Now I'm going to click start creating and there we go immediately a new one has been created here and I'm now getting redirected any second this is of course a bit slower in development but it's not going to be this slow in production don't worry. Let's just wait a second for the page to compile And what's important to know this is that they are immediately ordered by the sending.
So I'm gonna, you know, confirm, save. It's saved. I can now go back and I can see it's updated less than a minute ago. I can see the format here and when I click here it says confirm save and I can go ahead and change my my well the size of my canvas right and in here there we go now it says 900 by 900 but it looks like something did get messed up because... Oh, let's see.
Let me just confirm. Oh, I think I know what it is. Okay, okay, okay. My apologies. So this 900 by 900, I think that's just a completely random project that I've had from before.
What I want us to do is go instead of use update project in here, we also have a to do to invalidate the project's query. Yes, my apologies. So we can copy and paste this one and simply invalidate by projects and you can remove the second argument here. So the same thing we just did inside of here. You can copy the exact one here.
So this one is inside of useUpdateProject. I have modified the useCreateProject to invalidate on success and useUpdate to invalidate on success. So let me go ahead and try this out again. I'm gonna click on this one 900 by 900. I'm gonna change this to something even smaller.
Like this. Let's go out and there we go. You can see that it is immediately updated here. So now let's go ahead and let's add more than five of our methods here. So at least five but preferably more than that.
So that we can test out infinite load right. Because inside of our use get projects we added a limit of 5. So now if I click load more, there we go. You can see how I can get to my last one which is 900 by 900 and after the last page has been reached we no longer have that button available. Great!
Amazing, amazing job! We are now on to make the make a copy feature and delete feature.