{
  "video_id": "37v63U7-iG0",
  "title": "Build a Jira Clone With Nextjs, React, Tailwind, Hono.js | Part 2/2 (2024)",
  "url": "https://www.youtube.com/watch?v=37v63U7-iG0",
  "transcript": "You are watching part 2 of Jira Clone. You can find part 1 in the description. This is a quick reminder that you can also use my platform, CodeWithAntonia.com, and you can watch this entire course in chapters and you can also mark your progress. You can also become a premium member and you can unlock special courses like Canva Clone, as well as source code to over 20 premium projects and over 170 hours of premium content. Now let's go ahead and let's implement the project entity. So make sure you have at least one workspace. So I'm going to go ahead and create my code with Antonio workspace here in a random account that I have. And what we're going to do now is create associated projects inside of this workspace. And we will be able to reuse or at least copy a lot of things that we did for our workspaces. So let's go inside of databases. Let's go inside of Jira clone and let's create a new collection called projects. Inside of projects, let's create an attribute, which we're going to call workspace ID. And inside a size of 50 and let's make it required and click create. After that, we're going to have an image URL, which will be 1,400,000. Like this. So let's just write this correctly. I did. And it's not going to be required. All right. Let's go inside of settings and let's go inside of permissions and add a permission for all users to create, read, update, and delete projects. And let's click update. There we go. And one attribute that I forgot about, the name of the project. There we go. And it's required. And I put the size of 256. Great. Let's copy the project's collection ID now. Let's go inside of dot environment dot local and let's go ahead and add the projects id here there we go let's go inside of our config where we have all the shorthands and let's now add our projects id and i'm going to copy it from here there we go so now that we have that let's go ahead and let's create an api endpoint for that Inside of features, we're going to create a new feature folder for projects. And inside of that, another folder called server. And inside of that, a route.ds file. Let's go ahead and create a const app to be new HONO from HONO and export default app here. Then let's go back inside of our app folder API route. And let's go ahead and import projects from at features workspaces server route. And then just go ahead and chain route slash projects to projects. There we go. Now let's go ahead and let's build the first route, which will be to get all the projects which we need. So we're going to use a geth method here like this, and it will use a session middleware. And then it's going to have a Z validator here for the query. And it's going to use zod.object to validate workspace ID, which is a string. So we're not going to pass it as a param, but as a query. Let's go ahead and let's add our controller here. Let's get the user from the middleware. Let's get databases from the middleware. Let's destructure the workspace ID. from C request valid query. Let me just fix the extra space here like this. Let's go ahead and see whether we are allowed to load these projects for this workspace. So a way to get the member, which we can obtain from features, members, utils right here. Inside of here, I'm going to pass the databases, the workspace ID, and the user ID, which is our user.$ID, like this. If there is no member to be found, we are going to return c.json with an error, unauthorized, and a 401. Otherwise, let's go ahead and let's actually load the projects using await databases.list documents. Let me just fix the await typo here. Inside of here, we're going to pass in the database ID, the new projects ID, and then we're going to create a query from node app write starting with equal workspace ID, workspace ID, and then another query order descending dollar sign created at, because this is a built-in attribute from AppWrite collection. Make sure you have included the database ID here. Let's go ahead and move these global imports here. And let me just separate these things. There we go. Now that we have this ready, let's, of course, return C.json data projects here. Like this. Great. And now I want to load these projects somewhere. So we're going to go ahead and we're going to revisit our source components sidebar here. So after our navigation, we're going to add projects. So let's go inside of the components here and let's create projects.dsx. Let's export const projects here. and I will just return a div projects. And then I'm going to go inside of the sidebar and I will import projects from dot slash projects like this. So now you should be able to see them in the sidebar. So I'm going to go inside of my local host and there we go. After my navigation bar, I have an empty text projects. So what I want to do now is quickly revisit my workspace switcher here And I want to add this import for ri add circle fill and put it inside of my new projects here. And let me mark the projects as use client. This is also important. And then I want to copy the initial part here. So let me just copy this exact thing and paste it here. And let me just close the div. And I'm going to go ahead and add an empty on click here. So I just copied the exact header here from the workspace switcher. And instead of workspaces, this will now say projects. And it's going to be having its own little plus icon here. So we can add new projects easily. The first thing I want to do is load the projects here. So one thing that we didn't do is after we created our features project server route is create our projects API folder. And inside, we need a useGetProjects.ts. The easiest way to fill this is by going inside of Workspaces API, useGetWorkspaces. Let's go ahead and copy the entire thing here. Let's go back inside of the Projects API folder and paste this inside. I will rename this to useGetProjects. And we are immediately going to have to create an interface here. use get projects props and let's define workspace id to be a string here then let's go ahead and assign use get projects prop use get projects like this and inside of here we will get the workspace id our query key will be projects combined with the workspace id so that it reloads every time we change a workspace. And then we are calling client api projects dot get. And we are definitely missing the workspace ID. So I'm not sure why I'm not getting an error here. So let me quickly go back inside of my projects server here. So the only route that I have is this one z validator. So, okay, we can do it like this. If there is no workspace ID, we are simply going to go ahead and throw c.json error missing workspace ID 400, like this. Let's go inside of use get projects. And now we will just manually add a query here, workspace ID. And this will be failed to fetch projects. There we go. And now that we have use get projects here, we can go back inside of our components projects, which is used in the sidebar, and we can go ahead and get them. So data use get projects from our features, projects, API use get projects. And what we are missing here is the workspace ID, which we can get from use workspace ID, which comes from features, workspaces, hooks, use workspace ID. And then we simply pass in the workspace ID inside, just like this. And now what we are going to do is below this div encapsulating the title is iterate over our data question mark documents dot map. We are going to get the individual project. And then inside of here, we're going to define is active to be if path name is identical to open backticks slash workspaces slash workspace ID and then slash projects project ID. So we're going to have a project ID later. I mean, a project ID page later. And then inside of here, we can go ahead and we can return a link from next link. We can go ahead and give this an href openVectix slash workspaces slash well what we can do is we can just you know off it this Let assign it to a prop const href Mark it inside of template literals like this. And then we can compare the path name with an href. We are missing the path name, so we're going to add it. And let's just assign the href here. Make sure you have added the link from next link. and make sure you add use path name from next navigation. And then we can go ahead and add the path name here from use path name. There we go. The errors are now gone. We are missing the project ID though. So for now, I'm going to add const project ID to be null. And I'm going to add to do use the use project ID hook, which we will create later. Great. So we have the link ready here. Let's give this a key of project.$ID. Inside of here, let's open a div with a class name, which will utilize the CNutil here. We're going to go ahead and first define a set of default classes, which will be flex items center gap 2.5, padding 2.5. Rounded, medium. Hover, opacity, 75. Transition, purser, pointer. Text, neutral, 500. And then we're going to have is active. Background, white. Shadow, small. Hover, opacity, 100. And text, primary. Inside of here, we are going to add a span rendering our project name. And this span here will have a class name of truncate. Like this. So now, I think we can stop here because we cannot really see any projects. You can see this right now is for some reason loading my workspaces, it seems. So it seems like we have a mistake somewhere. So I'm calling use get projects here. Did I forget to do something? Use get projects. Client API projects dot get. Seems like I am not. So why am I getting projects here? Let's go ahead and let's revisit our source folder, our features, projects, API, use get projects. That's here. Let's check out the server here. So, I'm querying the projects ID, but for some reason, it seems like I'm getting back my workspaces. So, console.log projects. Let's see what's going on here. Can I expand this maybe then? Let's see why this is happening. I will just zoom out so I am always on my desktop here. What's worrying me is that I'm not seeing this console log here. So let me go inside of API UseGetProjects. And let me go ahead and see the data here maybe. Data is here, okay. And I do have documents called Code with Antonio, but this is definitely the workspace. So how come my workspaces are getting loaded here? Okay, let's go ahead and see how to debug this. One thing that comes to mind is that we have somehow put the invalid environment variable for our collection IDs. So let's see. My project's ID is unique. It's different from this. Okay, that's good. How about my config file source? Let's go ahead and go inside of config here. My project ID is looking at project's ID. Okay, that's good. And now let's go inside of here. The projects are definitely empty. I will just copy the project ID one more time. And this time I'm going to go directly inside of my features, projects, server, route.vs. I'm pretty sure that I'm having a hilarious bug somewhere. Perhaps you already noticed what it is. I don't know what it is yet. So let's see. Let's put this like this. Will that then give me anything different? No, it still loads the exact same thing. It's loading workspaces here. so then the project id is definitely correct all right so i have just realized where my mistake was i have to be more careful uh i don't think you any of you made the same mistake maybe you did if you followed really closely and did exactly what i did so this is the mistake inside of my source app folder inside of my API route.ts, look at my project import. It is exactly the same as my workspaces. And since we're using export default, it doesn't matter. I can call this workspaces too, right? I will not get an error if I do that. So I have to fix my import to go to projects like this. And then there we go. If I refresh now, hopefully no projects. Great. Okay. And now I can see, I should be seeing some console logs here. I'm still not seeing them. Okay. But at least we fixed that weird error. So be careful. Make sure you import your projects correctly. Don't be like me. Don't do it fast. Great. So now we are inside of the projects here, but I don't want to do anything further because we can't see them, right? So how about we go ahead and we create our form for that. And in order to do that, we have to start with the schema. So let's go inside of features, projects, and let's create schemas.ts. Let's go ahead and let's prepare z from zod. Let's export const create project schema here. And z.object here can be pretty much exactly the same, if not literally the same as the workspace schema. So copy the create workspace schema and simply add it inside. The only difference is that this will also have to accept a workspace ID in which to generate this project like this. So now let's go ahead and let's go inside of projects server route.ds. And this time we will be creating a post route. So it's going to be pretty similar to our post for creating a workspace. So let's go ahead and let's add a session middleware here. And then we're going to add a Z validator for the JSON, actually for form, because we will be uploading a file. And let's add a create project schema here. And then an asynchronous controller right here. We can now go inside of our previously created workspaces server route.ts and let's find our post method here. And I want you to copy the entire controller here. So the entire controller of the creation of the workspaces server route. and then go inside of projects server route and simply paste the controller here. And now we're going to slightly modify it. So we need the images bucket. Let's import that. We need the ID from node app, right? So make sure you have added images bucket and ID from node app, right? And now, first of all, we can remove the creation of the member. So no need for that. Let's remove that. And the image stuff will stay the same. But inside of here, we will be creating a project. So instead of workspaces ID, projects ID. And we are not going to have any in-right code. But what we will have is a workspace ID. And we're going to destructure this. So we can just leave it like this for now. and we will be returning back a project like this. So let's go ahead and fix some things. So first things first, from the form, we will also be able to destructure the workspace ID. Let's confirm that we did not name it image URL. We did not. So it's just image inside of create project schema. That's great. So we now have workspace ID, image, and name. And before we do anything here, Let's go ahead and see, are we allowed to actually create anything inside of here? So we're going to go ahead and pass in the databases, the workspace ID, and the user, which is user $ID, user ID. So if there is no member, we are not going to allow this user to create a new project inside of this workspace. But we are not going to be so strict so that we only allow admins. Anyone can create new projects. You can, of course, change that logic if you want to. There we go. That's it. That is our entire API endpoint for creating projects. And now let's go ahead and let's copy the Workspaces API Use Create Workspace. And let's put it inside of Projects API. Make sure you're inside of the copied one and rename it to Project. Use Create Project. Rename this to use create project. And fix the response and the request type to go to projects. Then go ahead and fix this as well. Projects. And everything else can stay the same. This will be failed to create project. This will be project created. And this will invalidate projects. And this will be failed to create project. There we go. So now what we have to do is we have to create a model and a form which will open up when we click plus here So let go ahead and do that So we going to go ahead inside of source features And instead of workspaces here, we can copy the use create workspace model and put it inside of projects. So inside of projects, we're going to create a new hook here, which we are going to call use create project model like this. Let's rename it to use create project model. The key here will be create project. And everything else can stay exactly the same. Now let's go ahead and let's copy the workspace creation form. So I'm going to, well, let's copy both the create workspace form and create workspace model. So both of them. And go ahead and set up projects and create a new folder called components. and paste them inside. So create workspace form and create workspace model. Let's first rename the create workspace form to be create project form. And you can click yes to update imports, and that will update them right here. So create project model. Whoops. Let me just see. Use create project model. Yes, this is supposed to be here. Use create project model. Use create project model. Oh, I see. Use create project model. Is that the issue? Cannot find the module. Okay, so we are working now inside of this copied create workspace model here inside of the project. And it looks like it cannot find this hook, which we definitely have. Hook. Oh, I see why. Because we called the folder hook inside of projects. I should have named it hooks. There we go. So now it will work. Okay. And then we'll just replace use create project model here and use create project model here. Inside of the create project form, I'm going to do a couple of things. So I'm going to rename the entire form here to create project form. So all of these three instances. And I will use the API use create project. And in here, I will use use create project. Inside of the schema, I will replace all four instances. So this one, this one, this one, and this one with create project schema. And again, looks like I named it schema when I should have named it schemas inside of the project. So just double check that you have updated the imports in here in your route for projects. So now let's go back inside of the create project form. We should now have schemas. There we go. So let's replace use create workspace with use create project here. And now inside of here, we're going to have to do some updates here. That's okay. And let's change this to create a new project like this. And instead of workspace name, this will be project name. So enter project name, the first input. And this will be a logo. but instead of workspace icon, this will be a project icon. Like this, and inside of here, it will be create project. Like that. There we go. So now there is one more thing that we're going to have to add here, and that is const workspace ID, use create workspace ID. We'll use workspace ID. from features, workspaces, hooks, workspace ID. Let me just move it here with my features like this. Now we have workspace ID here. And then when we submit here, we can also add workspace ID here like that. And now inside of here, once we create, I'm just going to go ahead and close this. And instead, I will add to do redirect to project screen. We don't have it yet. Now we have to resolve why is this error happening here. It's happening because of our use create project. So go inside of use create project. And in the response type, make sure you are only looking at the successful result here. So add 200 in the response type here. And then you will no longer have that error right here. You can see data exists, but we don't even need it for now because we're not redirecting anywhere at the moment. Great. So we've wrapped up create project form. Now let's wrap up the create workspace model and start by renaming it into create project model. Then let's go ahead and import create project form. Let's add create project form here and let's change this to create project model. Now let's use the create project model by adding it inside of source app dashboard layout. And let's add it just below create workspace model. So I will import this from features, projects, components, create project model. So we copied those two components from workspace because they're pretty similar. So now we have to revisit our workspace switcher component. so we can simply get the use create workspace. Okay, we don't have to. Let's directly go inside of projects, inside of our source components. So this projects, which is used inside of navigation, right? And inside of here, we are simply going to go ahead and add this structure open from use create project model. Like this. And then we can simply add open here. And what that should do, I'm going to refresh and click here. and there we go create a new project and if i did this correctly let's see once you click create project it looks like nothing is happening and i think that that is because we right now what's happening is that we can't submit the form because we have some errors and what errors well if you go ahead and go inside of your features projects schemas we defined workspace ID to be required. But it's not something that we're going to type in, right? We're just going to infer it from the URL. So let's go inside of components, create project form. And inside of here, when we do the Zodra resolver, create project schema, let's go ahead and see, Can we do .omit workspace ID? True. Can I do that? And I think that then if I do test, okay, it says fail to create project, but it allowed us to submit. So that's a good thing. Looks like we are doing invalid document structure, unknown attribute user ID. All right. So what's happening is that since we copied the controller from creating the workspace inside of our app full, my apologies, inside of the features, projects, server route, we are assigning the user.id, I believe, inside of the new project. There we go. But that doesn't exist. So not something we're going to pass. Only name, image URL, and workspace ID exist. Third time's the charm. Test project. And let's click create project. Project created. There we go. and immediately shown here in my projects, because once we do, instead of API use create project, we invalidate queries projects. And those are the use get projects here. So no matter, because if we don't pass workspace ID here, it doesn't matter because this will reinvalidate all projects. You can pass additional options like exact to be true, and then you would have to pass the workspace ID, but this is just simpler for us. Oh, I have a typo here. So failed. Okay. What I want to do now is the following. So I want to go inside of source, inside of my features, projects, components. Let's see, create project model here. We are doing on cancel close here. Okay. That's good. Okay. I know, I know what was bothering me. That's fine. But what we need to do is we need to go back inside of our components, especially inside of projects here. And what I want to do is wrap up how this looks here. So I also want to show the project avatar so that we can test out image upload and confirm that it works here. So let's go ahead and let's use our workspace avatar because this one will be the most similar. So let's copy the workspace avatar and let's add it inside of our projects feature components. So instead of workspace avatar, this will be Project Avatar, like this. Let's rename the instances of Workspace Avatar inside of Project Avatar, like this. And everything else can stay the same. And then let's go back inside of Projects here. And alongside our span, above it, we're going to add Project Avatar. We're going to give it an image of project.imageURL. we're going to give it name project.name. Make sure you have imported the project avatar from here. And now let's try it out. There we go. Now it says test project here. The problem is it's a little bit big. So we're going to change the defaults for the project avatar. Let's go inside of the project avatar here. And let's also introduce a fallback class name here, just as we did for our member avatar. So it's optional. Let's add fallback class name here. This is fine, but let's just change the size here to be 5 by default. And let's change the avatar size to be 5 by default. And inside of the avatar fallback here, let's go ahead and wrap this entire thing inside of CN as we have above And then this will be the first argument let me just scroll this and then we going to pass in our fallback class name as the second argument like this there we go so now project avatar should have a smaller text like this and let's also just change the default text in the outer fallback to the text small. There we go. So now we have a text test project here. So I can now create another one. And let me go ahead and upload an image that I have here. So let me just find the image that I use. There we go. Let's click create project. And let's hope this is still working. There we go. we have a project with our new image here. So the only thing that's not happening right now is the model closing. And that's okay because we're going to add on submit to redirect to the project ID. So the cool thing is that now if I go ahead and create a new workspace, so another workspace, you can see that in here I have no project. So this is a special project, right? You can see that I can add that here. But if I go back to this one, it doesn't exist, right? So only projects inside of this workspace exist. Great. So what we're going to do next is build the individual project ID page. And then we're going to go ahead and create the settings for the project. And then we can finally create the tasks. Great, great job. Now that we have the projects, let's go ahead and let's build the individual project ID page. So right now in my sidebar, when I click on a specific project, my URL is localhost 3000 slash workspaces, workspace ID slash projects, and then null. So first thing, let's fix that null. Let's see where did I go wrong in doing that. So I'm going to go inside of my source components and inside of my projects here. and first of all, let's go ahead and see. So my link here, Ahrefs, leads to slash project ID. It should go to slash project dot dollar sign ID. That's the first thing I have to fix about my Ahrefs here. So now I think the page will still be 404, but I expect, there we go. now in my URL, I have the actual ID of my project. Great. Now let's build that page. So we're going to go inside of source app folder dashboard inside of the workspace ID. We're going to create a new folder called projects. And inside of that, another folder, which we're going to call project ID like this inside of brackets and then a page .tsx inside. So this will be a project ID page. And let's go ahead and let's define import, use, sorry, get current from auth queries and import redirect from next navigation so we protect this route so let's turn this into an asynchronous method const user will be a way to get current if there is no user we will just redirect to slash assign in great and now what i want to do is i want to create an interface here project id page props and we're going to go ahead and have params project ID string. So let's go ahead and let's assign project ID page props here and let's destructure our params here. And I think that now I will have my project ID here as params project ID. There we go. So now depending on which one I click, my project ID changes. And looks like the is active works. I'm just not sure how it works. Let's just go back to my source components projects. Why does it work? So is active. Oh, if path name is href. Oh, okay. So what did I need the project ID for? I am not sure. Let's remove this then. We are not going to need it. So it looks like our current solution, checking for path name and the href works just fine. So now what we have to do in this project ID page is load the project information. So we can do that by creating use get project, or since we are in a server component, and since we are loading just information, right? So I don't like to use a server component to load the data when it needs something that will be, you know, in an infinite load or some kind of pagination. So for example, our tasks will be loaded in a special component purely from the client side using the API. But for the project information, let's go ahead and let's create a database query for that. So we're going to go ahead and go inside of source. And inside of features workspaces, we have this queries here. Specifically, we have the get workspace. So let's copy the interface, get workspace props, and the export const get workspace. So I want to copy that. And then I'm going to go inside of projects and create my own queries.ts, which are used for server components. And let's go ahead and change this to be get project. And we're going to have ID. Let's call this project ID actually. Let's be specific. So this will be get project, project ID and get project props. So we are creating the session client like we usually do. We are obtaining the member from dot dot slash member utils here. What I like to do is go to features member utils. And now before I can check whether we have the member or not, I actually want project first. So this is how I'm going to do that. I'm going to go ahead and define this to be const project, await databases, get documents, and we are missing this type. So we're going to create it. So first let's add the database ID. Let's replace this with projects ID and let's replace this with the project ID like this. So make sure you have added this config here. Now let's go ahead and let's copy from workspaces types.ts, copy this and paste it inside of your projects. So inside of here, let's call this a project, which will have name, image URL, and it will not have the invite code. And instead of user ID, it will have workspace ID. There we go. So now we can go back inside of the get project query. And instead of assigning workspace type to this get document, we can call the project from dot slash types because we've created it right here, right? Great. And now what we have to do is use the newly loaded project here to get project dot workspace ID. So then we are first going to load the project. And once we get which workspace ID is this project related to, we're going to decide whether to show that to the user or not. So if there is no member, we return null. Otherwise, we return back the project. There we go. So now we can go ahead inside of our app folder, inside of dashboard, workspaces, workspace ID, projects, project ID here. And we're simply going to set const initial values to be await get project, which you can import from features project queries and simply pass in the project ID to be params project ID. And then let's go ahead and do the following. Let's just do JSON.stringify initial values here. And there we go. And now you can see that we have the exact project and all the information about the project here. So now we're going to start building our UI from this. So I want to go ahead and give this div a class name. Flex, flex, column, and gap Y of four. I'm going to remove this. And then inside of here, I'm going to go ahead and give this a class name. flexItemsCenter and justifyBetween. Then instead of here, I'm going to create a div with a class name flexItemsCenter and gapX of two. And then I'm going to add a project avatar inside. So just import project avatar here. And now we're going to give our project avatar a name of initialvalues.name. and of course yeah let's just do if there is no initial values but what we can actually do is we can throw a new error here project not found that's what we can do so we can handle errors properly instead of just returning now so i will go back to where we usually return now and i will fix that so we're going to pass in the name here otherwise let's go ahead and also pass image to be project.imageURL. I apologize, initialvalues.imageURL here. And class name will be size8. Like that. And then after that, we're going to add project.name. Again, initialvalues.name. And a class name, text large and font semi-bold. So there we go now. Once you are on the individual project page, you will see a nice header here telling you on which project you currently are. Great. So now let's go outside of this div here and let's go actually let's outside of this div and let's create a new one which will encapsulate our button which we can import from components UI my button like this, and this one will simply say, edit project. And we're going to go ahead and give it a pencil icon from Lucid React.\nI'm going to move this to the top here. And I'm going to go ahead and give the button here a variant of secondary. I'm going to give it a size of small. And I'm going to give it as a child because it's going to become, yeah, so let's immediately fix this by adding a link from next link. because this button will simply redirect to href slash workspaces slash initial values dot workspace id slash projects slash initial values dot project id uh well that's actually id right and then slash settings that's where it's going to go like this and let's give the pencil icon at class name size 4 and MR of 2. There we go. So we now have the edit project button right here. Great. So now we're going to go ahead and do that. We're going to create the edit functionality. So what I quickly want to do before we proceed is kind of standardize my decisions about this. So I want to throw an error if initial values here cannot be found, right? So here's how we can handle that. Inside of our app, we can create a global error.dsx, I believe. And inside of here, we can create an error page, a div class name, minimum. Let's just do height screen. And inside of here, let's add an alert triangle from Lucid React. And then a paragraph, if something went wrong. And like this, with a class name, text small, and let's go ahead and give it text muted foreground. And let's give this a flex items center and justify center. And let's also give this a flex column. And the error page also needs to be a client component, so let's fix that. And let's also give this a gap Y of two. And then below this, let's add a button from components UI button with a variant secondary back to home. And this will also be a link from next link, which will simply allow the user to go back to the home page like this. So let's try and demonstrate this page now. So I'm going to go ahead inside of my project page here. And inside of my get project, I will attempt to throw an error. So instead of catching this and returning null, I will not do try catch at all inside of the get project. So I will just do this. And if there is no member, I'm going to throw an error. Throw a new error here, unauthorized. So handling it the same way that I would an actual API route. So this time, I'm going to do this. Before I even try anything, I will throw new error test. And let's see whether that will get caught. And there we go. So we get redirected to this page. Something went wrong. So let's go ahead inside of error and let's just fix it a bit. So perhaps we don't need text muted foreground here like this. Maybe we can increase the gap Y here. And maybe we can give this a class name of size 5 or size 6. Make it a bit larger. Okay, let's refresh again. There we go. So something went wrong. And let's give this button a size of small. Okay, so now something went wrong. Back to home. And we will get redirected to here. Great. And these are, you know, the logs of those errors. Okay, that looks fine. What we can also do, I believe, is copy this error. So this will catch, you know, all errors inside of the entire app. but I think we can kind of do it just inside of the dashboard or specifically inside of the workspace ID if you want to. So if you want to, I think you can put it like here, I think. And then if you get an error, so let me go back to a project. There we go. You can see that now this is how the error looks like. It's kind of in a lower level. So if you prefer that, you can do that. So go ahead and put your error inside of the dashboard layout here like this. So we're just playing around, you know, with server components. So you'll learn better of how they work. But I have a feeling that I will forget to do that. So I'm going to remove it from the dashboard. If you want to, you can. I'm not going to waste too much time now on doing those things because I just want to have one global error handler here. And I also want to have one global loader. So I will copy this with also loading.tsx and this will be loading page. And this will be very similar, but it will not have any text. Instead, it will just use a loader from Lucid React, size 6 and animate spin. And no need for any gap Y4 here. No need for this at all. No need for this. No need for this. Like that. And then I'm going to go ahead and try and go inside of let's see do I have any existing loading ones looks like I don't maybe in the dashboard I do have loading here so I'm going to remove the loading from here because I just want to see something trigger that initial loading let's see let's go back to home there we go so that was that initial loading right uh okay so that's how you can handle you know those error states and things like that there we go okay maybe that's a bit too much loading uh too high level of loading so okay let's make it look properly so first thing i want to do is give this a text muted foreground like this because i don't like how uh dark it looks and then i'm just going to go ahead and copy this exact loading and this exact error, but I'm also going to give this error the text muted foreground here and this one text muted foreground. So I'm going to copy both the error and the loading, those two, and I'm going to put them inside of each of my route groups. So out is going to have its own error and loading. Dashboard is going to have its own error and loading. and standalone will also have its own error and loading. So I think that now all of this will look just a bit nicer. There we go. The loading is now here, as you can see. And if any errors happen for whatever reason, they will also be handled here. But I think that most of our queries at the moment are not handling those errors. So let's fix that. I'm going to go ahead and find all queries that we have, starting with my auth queries here. so okay actually this is fine this one is fine because we always follow it with a redirect so okay this is good now let's go inside of some other queries like workspaces here so let's use the get workspaces one and let's simply remove the try and catch so we are just going to throw errors so in case members total is zero here uh what we will do let's see uh get workspaces okay maybe this is this is not the case for an error so that's fine yeah this is okay but no need for try and catch either if an error happens i want to catch it instead of the individual get workspace let's remove the try and catch here. Let's indent it. If we are not a member, we will throw back new error unauthorized like this. And then let's do the same thing in to get workspace info props. So remove the try and catch here as well. And well, I won't throw any explicit errors, but if they happen, they will be caught as well. And I think I have queries. So I think only one more in the project and we are handling that well here. Great. Okay. So now we are explicitly handling all of our loading states and we are also handling our error states. Great. There we go. So let's go back inside of our project here and now let's go ahead and let's build the settings screen. So we will also be able to copy and paste a lot of things from our workspace. But let's start as usual. Let's start with the schema. So I'm going to go inside of my features, inside of my projects, and I'm going to go inside of schemas.ts. And now I'm just going to copy and paste this like that. And I'm going to call this update project schema like that. And the only thing I'm going to change is workspace ID. I'm going to remove it and I'm just going to change that optional here for the name. And let's change this to minimum one character required. This is fine. And now let's go ahead and let's go and create the patch route. So we can copy from features, workspaces, API, my apologies, server, route.ts. We can go ahead and copy the patch route here because it's quite similar. So let's go ahead and copy this. And let's go inside of projects, server route. And after our post and after our get, let's go ahead and let's change this copied one. So this is now, of course, for a workspace, but we're going to modify it to work for patching the project. so let's start by changing the patch to go to project id param let's change the z validator to use the update project schema which we can import from dot dot slash schemas like this now let's go ahead and let's first attempt to destructure the project id here and before we can do the whole member thing we first have to get the existing project by using await databases dot get document. We are defining that we are getting the project, which you can do by adding dot dot slash types, which we've recently created. Let me go back to my patch. Here I am. And then we're going to pass in the database ID, then the projects ID, and then the project ID. So we are basically getting the project first, and then we're going to go ahead and use the existing project here for the workspace. So existing project There we go And then if we don have the member like this so we going to allow any member to you know change projects It's only workspaces that we are restricting to admins. So the only check we're doing here is whether you are a part of this workspace or not. And the rest can pretty much stay the same. What we have to change is the last thing. So we are updating projects collection here. and we are using the project ID like this. And let's go ahead and return the project back. Great. So we have that. Now let's go ahead inside of API. Let's copy use create project. Let's rename it to use update project. Let's go ahead and rename the hook use update project. Let's modify the response type and the request type. So API.project, and I'm going to add additional project ID, and then we have to modify the method to be patch. Keep the 200 here, so we are only working with the correct response type, and then we have to extend this with the correct project ID as well. Change this to patch, and we are also going to have to extend this by alongside form, also passing a param. And let's go ahead and add it here. Let's change the failed to create to failed to update. Project created will be project updated, and this will be failed to update project. In the on success, we can destructure the data, and alongside invalidating the projects, we can invalidate individual project and then data.$ID. So this exact page will get updated. And also what we can do since we are fetching using server components is we can add our router here from next navigation. And we can also on success do router refresh so server components get updated as well. And there is one inconsistency that I just got reminded of here. So let me see. I use router.refresh somewhere else in my code. let's see, use login, use, okay, inside of my edit workspace form. So this is what's bugging me. What I want to do is I want to go inside of my reset invite code here. Is this the only place? Let me just double check. Router refresh. Okay, it looks like this is the only place. So let's go inside of reset invite code, use reset invite code, and we can simply go ahead and add it here, router.refresh in that case. And we have to add our router, of course. So const router is use router from next navigation. Always double check that you didn't import from next router accidentally. There we go. And then you don't have to do it here. So no need for anything special in the on success barang here. Great. The only thing that's kind of bugging me here is that in my edit workspace, okay, so this is edit workspace form. On submit, okay, so we redirect the user back. Okay, so what I want to do is I also want to go inside of use edit workspace form or use update workspace hook. And I want to do the same thing here. So the server component is updated. So use router from next navigation. So this is use update workspace, right? And I'm going to do the same thing here. So router.refresh like this. And instead of my edit workspace form, there really is no reason for us to redirect to the individual workspace ID. We just have to, you know, reset the form like this. And let's see, do I need the router then? Looks like I am still using the router for something else. Okay. But yeah, after we submitted the edit workspace form, no need to redirect anywhere, right? We can still stay there and do more changes if we want to. Okay. So now we have the hook to update the project. We have the API to update the project, but we don't have the page. So let's go ahead and do the following. Let's go inside of our app folder, inside of dashboard. My apologies, inside of standalone, workspaces, workspace ID. And then in here, we're going to create projects. And then in here, we're going to create individual project ID. And then in here, the settings page. So pretty much the same thing that we did for the workspace ID settings. So once the user clicks on edit project here, they will get redirected to the settings page, which will be in this kind of layout. This one, right? So this will be the kind of layout that we are going to load. So let's go ahead and do that. We're going to go ahead inside of the settings. Let's go and add a page.dsx inside. Let's add a project ID settings page. Div like this, settings page. and then I think that already this should work if I click on a project and I click on edit project it should no longer be 404 so my url is localhost 3000 workspaces workspace id projects project id settings so double check that you have the same structure in your standalone folder here standalone workspaces workspace id projects project id settings page and also double checked that in your dashboard, workspaces, workspace ID, projects, projects ID, page, you have created the correct redirect here. So workspaces, workspace ID, projects, project ID, settings. That way, it cannot not work. Okay. So what we have to do inside of here is the same thing that we keep doing. So first of all, we have to get the user using a way to get current. if there is no user we are not throwing any errors but we are redirecting to sign in then we are getting the initial values here using a way to get workspace from my apologies get project from feature projects queries and we have to create an interface project id settings page props which hold the params project id which is a string inside of here we're going to the structure project ID settings page props. You can see how this is throwing an error, for example, because we forgot to pass in the project ID here. So let's get the params and then let's go ahead and pass in the project ID here to be params project ID. And there we go. No errors now. Great. So let's go ahead now and let me just see. Oh yeah. So the cool thing now is that initial values can almost never be null. That's cool. Okay, so now let's go ahead and actually build the settings page here. So the first thing we have to do is we have to give it a proper width like we always do. So for example, in my workspace settings page, I have this. So let's go ahead and do that. There we go. And because I just went here, let's go ahead and just fix this. So go inside of standalone workspaces, workspace ID inside of settings. So inside of workspace settings here. And this time, since we are throwing errors directly here, right, we don't allow the workspace to be null because I think this will also throw an error if it doesn't exist. So this will never happen. You don't need it at this point. And this is just a random TypeScript restart TypeScript server. There we go. Now it goes away. Okay, so now we will pretty much have to do this, right? So we have prepared inside of projects, project ID settings page right here. We have the initial values and now we have to create the edit project form, which you guessed it, we can copy from the existing edit workspace form. So let's go ahead and let's close everything. Let's go inside of source, features, workspaces, components, and edit workspace form. I'm going to copy that and I'm going to go inside of projects, components, and I'm going to paste that here. So this will become edit project form. There we go. And now I'm going to go ahead and, well, we have to resolve all of this, but let's start with renaming the props and the constant name. So this will be edit project form. Like that. It will have on cancel and initial values. So that stays as it is. Now let's go ahead and modify the schemas to use the update project schema. Instead of use update workspace, it will be, well, use update project import. You're calling the use update project method. We don't have the delete one. We don't have the reset one. And instead of the type workspace, the type project is here. So let's go ahead and simply prepare the use update project here. And I want to just comment out the delete one, but we can entirely remove the reset invite one. And we can also entirely remove the use confirm for the reset invite link because there will be no invite link here. Let's change the instance of use of update workspace schema here and here and I believe in the submit method here with update project schema. So this is resolved. This is resolved. And this is resolved. The handle delete for now will be commented out. So it will just open the confirmation model. Nothing more. The handle reset invite code can be deleted entirely here. The on submit here will pass the project ID like this. Everything else can, I believe, stay the same. The handle image change can also stay the same. We will not be using full invite link. We don't need handle copy invite link. We don't need the reset dialog anymore. And inside of here, initial values that name is just fine. And let's change this to be project name and the placeholder enter project name. And this will be project icon. And then let's go ahead and let's simply go into this second card in between the danger zone and the first one and remove this. And the danger zone can stay here. We're just going to remove this, what we don't have, and this will say delete project. And the danger zone will say deleting a project is irreversible and will remove all associated data And let see what else is there to fix The initial values will be a type of project There we go. Now we have edit project form props here. And then what we can do is we can go inside of our standalone right here, inside of workspaces, workspace ID, projects, project ID, settings page. And inside of here, we can render our edited edit project form now. We can go ahead and give it the initial values to be initial values now. And no errors. Great. So if I refresh this, there we go. I can go ahead and modify my project. Let's see if this now fully works. So I have this project here. Let's see. Is this still loading? Let me refresh. Oh, that's weird. So I'm getting 401 here. Okay, we will debug what's happening here. Let me go to Logo Host 3000. Okay, now everything seems to be working just fine. So I will create a new project called Test. And I will go inside of this project and I will click Edit Project. There we go. It's called Test. And I will click on here, Test 2, and click Save Changes. And there we go. Test 2. Updated. Great. And every time I click back, I seem to be having this issue. That's weird. Yeah, I'm not sure why I'm getting redirected here. So let's go back inside of the edit project form. And I think that my back button here is incorrect. Yes, my back button here goes to slash workspaces. And then it loads the initial values dot ID, which is the project ID. So it gets confused. So inside of here, we have to go to slash workspaces slash workspace ID. And then we have to go to slash initial values, whoops, dot ID with the dollar sign. There we go. So now we will not be having those things happen again because this ID that's currently in the URL is the project ID. So it's confused. Okay. Now I accidentally entered something. Let's go back to localhost. I get redirected to workspace ID. I click on a project. I click on edit project. And if I click back here, again, oh, I forgot one more thing. So again, back in the edit project form, in the router push, we are missing slash projects. Like this. There we go. So let's try third times the charm. On the project settings, if I click back, there we go. I am onto that project. Perfect. So now let me try again, save changes. There we go. I'm not a big fan of this refreshing thing, which happens, but I think that happens because of, if I go to use update project, it happens because of router.refresh. And currently that's the only way we can get the newest data, I believe. So let's leave it at this. Okay. Now let's go ahead and let's implement the delete project so we can wrap up the chapter. So we can pretty much copy the same thing we have inside of our WorkSpaces server as always. So let's simply copy the entire delete workspace ID here like this and let's go inside of projects server route.ts and after our patch to update let's now chain a delete method. Instead of workspace ID, we will be looking for a project ID, so project ID. And the only difference is that we are first going to have the existing project. So I'm going to copy this from the row above, and I'm going to paste it in my new delete here. So I'm going to first attempt to get the existing project, and then I will use the existing project here. So existing project.workspaceID. And let's go ahead and remove this because anyone will be able to remove a project. Again, I don't want to be strict with my project. And then I will simply call this delete tasks, right? Because in each project, they will be tasks. So let's do await databases, delete the document, and let's go ahead and pass in the projects ID here. And let's use the project ID and let's go ahead and pass in the project ID here or we can use existing project.id to be more specific. There we go. We now have this and now let's go inside of API. Let's copy use create project and actually let's copy use update project. Let's paste it and let's rename it to use delete project. Inside of use delete project, let's rename this to use delete project and now let's fix the response type and the request type to use the delete methods. Let's fix this here as well. So delete method and we are not passing any form here. So remove form. We are completely relying on the param and this will be fail to delete project, project deleted. And let's also do projects here and project here is fine and failed to delete project. Great. So now let's go ahead back inside of our projects components, edit project form. And I believe I have commented out one hook. So let's go ahead and add use delete project, which we now have. So let's just wait for this to resolve. And then I'm going to uncomment this and simply change this to use delete project. I will change the mutate to be delete project and this will be is deleting project. Let's go ahead and change the dialog to be delete project. Let's fix the handle delete here. I'm going to uncomment it and I'm going to call the new delete project here and I'm going to call the project ID here and where I'm going to go is, well, yeah, we can go back here. That's fine. Or we can go back to, let's see, do I have access to, I don't have access, oh, I do, inside of initial values. So, we can go inside of slash workspaces and then initial values, workspace id we can go there okay let's try it out now so if i click delete project and confirm and there we go and if i go here again click here first let's let's rename and let's upload an image for example so i'm going to use the unsplash image here Click Save Changes. Let's see if that is working. There we go. Let's rename. Let's rename. Perfect. And let's click Delete Project and Confirm. And there we go. Our project has been deleted. Great. So one thing that I want to do now to wrap it up is that when I create a new project from the form, from the model, I want to get redirected there. So let's go inside of the Create New Project. create project form and inside of here we have the router here and i added the comment to do redirect to the project screen so let's add in the on success mutation in the on submit in the create project form router.push and we're going to go to slash workspaces and then i'm going to go ahead and go to we have workspace id create so workspace id slash projects and then i will restructure the data from the on success here and I will use the new data dot dollar sign ID like this. There we go. Let's try it out now. So new project, I will press enter and I'm redirected to that new project. Amazing. We now have full control over our projects here and now we are ready to go ahead and create some first tasks. Great, great job. Now that we have the individual project ID page, we can go ahead and create a component called a task view switcher. This component will serve as a curation of all the tasks that we have. And we can switch by looking at them through a table, a Kanban board, or a calendar. And that component will also have its own new task button. And we're going to reuse that component here in the individual project page, and also later in the My Tasks page. So let's go ahead and handle that. We're going to go ahead inside of the source, inside of features. And inside of here, I'm going to go ahead and create a new folder called Tasks. And inside of here, I'm going to create a components folder and inside a task view switcher.tsx. Let's go ahead and let's export Tonst Task View Switcher. And inside of here, let's return a div tasks. Now let's go back inside of our app folder, dashboard, workspaces, workspace ID, projects, project ID. And then what we're going to do is after we render this, we're going to render the task view switcher like this. And there we go. should see the text which says tasks right here below this new project header. Let's go back inside of the task view switcher and go ahead and confirm that inside of your, let's see, inside of your components UI, you have tabs. So we need tabs right here. If you don't, you can go ahead and go Bunex-Bunex-Chatsien 2.1.0, add tabs. Great. So I have tabs right here. So I'm going to go ahead and add tabs here. And let me import them from components UI tabs. And while we are here, let me add everything else we need here. So we need tabs, tabs content, tabs list, and tabs trigger components. So we're going to go ahead and just give this a class name. Plex1, pull width, border, rounded, large. Then inside of here, I'm going to create a div with a class name, heightful, flex, flex column, overflow auto, and padding of four. And then inside of here I going to create another div with a class name flex flex column gap Y2 on large flex row justify between and items center Inside of here we going to create tabs list component with a class name full width on LG with auto Inside of here, let's add a tabs trigger component, which will say table. The class name here will be height 8 with full LG with auto. and value will be table. Now let's go ahead and copy this two times. The second one will be value Kanban and the text Kanban. And the third one will be calendar like this. So now we have table, Kanban and calendar. And this is how it looks like on desktop. Now let's go ahead and let's style our tabs component so it suits our design as required. So we're going to go directly inside of the tabs component here. So that's located inside of source components UI tabs here. And first things first, we're going to go directly in the tabs list. And what we are going to do is go ahead and ensure that we have background muted here. And let's change it to background transparent. like this. So background is now transparent and we're going to add gap X2. So these are spaced out between one another like that. Great. Then we're going to go ahead inside of the tabs trigger component here and we're going to go ahead and find the text small here and after that add BG BGNeutral100. That's what we're going to do. And after BGNeutral100, we're going to go all the way to the end where we can find the states. And by states, I mean this one. So, data state, state active. So, change this from background, from BGBackground to BGNeutral200, like this. and state active here will be text black like this. And let's go ahead and let's remove the shadow. We don't need the shadow like this. There we go. So I've just slightly modified our table. If you don't want to modify it, you don't have to. But this just suits my idea a bit better. Great. So we now have that. Now let's go ahead and continue developing here. So besides the tab list here, we're going to have a button component from components UI button. So make sure you have added that import here, which will simply give you an option to create a new task. So we're going to write new here. And we're going to go ahead and do plus icon from Lucid React here. We're going to give this button a size of small. We're going to give it a class name of with full LG with auto like this. and we're going to give the plus icon a class name size 4 and MR of 2 like this. There we go. So this is how it looks like on desktop, a nice little button here. Great. And what we have to do next is go below here and add dotted separator here. So make sure you have imported dotted separator. Let me just move this in its correct position. After the dotted separator, I'm going to add a comment, add filters. And then inside of here, we're going to have another dotted separator here. And let's ensure that both of these dotted separators have a class name of MY4. So let me just remove some extra space here and add a space here instead like this. And then what we're going to do here is we're going to go ahead and add a fragment because we are later going to add this in a conditional ternary. So it's easier to add a fragment already. So add tabs content here and give this tabs content a value of table and give it a class name of margin top zero. So in here, I'm going to go ahead and write data table. Then I'm going to copy and paste this. This value will be a Kanban. So I'm going to write data Kanban. And here I'm going to give you a value of calendar. So this will be data calendar. There we go. And you can go ahead and write data filters here. So you can see our skeleton now. We're going to go ahead and have the Kanban and the calendar. And we're going to develop these components individually. And inside of here, we're going to have some static filters, which will apply for each of these states here. So what we have to do now is actually create our task. In order to do that, let's go inside of console. Let's go inside of our JIRA clone database. And let's create a new collection alongside projects, members, and workspaces. So I'm going to call this tasks. I'm going to go ahead and create this. And I'm going to create some attributes. Let's start with the workspace ID, which will have a size of 50 and it will be required. Make sure you don't misspell workspace ID here. After workspace ID, we're going to add a name. I'm going to limit this to 256 and make it required as well. After this, we're going to have a project ID with a size of 50 and it's also going to be required. After project ID, I'm going to add an assignee ID, which will again be a size of 50 and required. After that, I'm going to add a description, which will have, well, let's say 2048 characters max, and it will not be required. After description, let's add, well, we're no longer adding any strings here. Now let's go ahead and let's add a date time, which we are going to call due date, and it will be required. Then let's add an enum, which will be called status. And the possible elements inside of here will be backlog. Make sure all of these are in uppercase letters. So backlog to do, in underscore progress, in underscore review, and done. And it will be required. And click create. All right. And then we're going to have not a string but an integer called position. And inside of here, we're going to go ahead and create a minimum position of 1,000. and let's go ahead and create a maximum position of a million. Or yeah, let's go ahead and do a million like this. And this will be required as well. And let's click create. So why does it start at thousand? So the position we'll be using instead of increasing each task by a plus one in the position, we'll be increasing by a plus thousand. So we have a lot of room to fix things in case something goes wrong, right? So let's go ahead and confirm it like this. This is the position. So you should have the workspace ID, name, project ID, assignee ID, description, due date, status, and position. Out of those, only description is not required. Let's go ahead and copy the tasks ID. Let's go inside of .environment.local. And let's go ahead and create our next app write tasks ID. Now, let's go inside of the config file. Let's go ahead and define our tasks ID. Let me copy it from here and add it here. There we go. Now that we have that, let's go ahead and let's start creating our endpoint for creating tasks. So in order to do that, let's first prepare by creating the task schema. So let's go inside of our existing tasks feature folder, where for now we only have the components task view switcher. And let's create schemas.ts. Inside of here, let's export const create task schema. Let's import zod. And let's go ahead and define the name to be ZString with a minimum value of 1 and required. Let's go ahead and give it a status of z.nativeEnium. And inside of here, we'll have to add a task status. So let's go ahead and do that by going inside of tasks and creating a new types.ts. Let's export Enium task status to be backlog. So this has to match 100% what we wrote in the app, right? right so make sure you don't misspell and you can you can go ahead and do this you know just have your status opened click on update here and you just double check that you're doing it correctly so it should have backlog to do it should have in underscore progress in underscore progress it should have in underscore review it should have done like this so let me just double check all of these are the same great then we can add the task status from types to the native enum right here great and let's go ahead and extend this Can I just pass in required here as well? I can't. Okay. So I have to add required error required. After status, we're going to have a workspace ID, which will be ZString and Trim. Also, let's add Trim here. Minimum one required. Same thing for project ID. due date will be z.course date without executing course so just date and optional actually not optional it's required and assignee id will be z.string trim minimum one and required and then we're going to have description z.string.optional and we're not going to add position because we will not be passing that\nthe form so we're going to calculate that ourselves great we now have the create task schema so we can go ahead inside of the tasks feature folder and create server and inside server we can create a route.ts let's go ahead and create const app to be new hono from hono and export default app let's go ahead inside of our source app folder apiroute.ts let's carefully import tasks from features tasks server route and then let's go ahead and chain route tasks to tasks Now let's go back inside of our tasks server here and let's implement a post method for creating new tasks. So we're going to have a session middleware here and then a Z validator, which will look for JSON and create a task schema. After that, we're going to have an asynchronous controller here. Let's go ahead and let's extract user from c.get user. Let's extract databases from c.get databases. Let's go ahead and let's destructure everything we need from cRequestValidJSON. So that will be name, status, workspace ID, project ID, due date, and assign me ID. We are then going to get the member by using a way to get member from features, members, utils. Let me go ahead and align everything the way it needs to be aligned. Okay. So we're going to get the member here by passing in the databases, the workspace ID, and the user, which is my user.id, and I mean user ID here. If there is no member, we are not allowed to create tasks inside of this workspace. So return c.json error unauthorized with a 401. Now let's go ahead and get the highest position task. So const highest position task at the moment will be await databases.list documents. passing the database ID and then passing the tasks ID, which we haven't used yet. So we have imported it from the config tasks ID. And now we're going to go ahead and do a query here. So query.equal, we have to import query from node app, right? So just ensure that you have added this as well. So query equal, status will be status. Query equal workspace ID will be workspace ID. Query order ascending will be by position. And query limit will be just one. So we are just looking for the highest task. So calculating by that highest task, we can get the new position. So const new position will be highest position task dot documents dot length is larger than zero. In that case, highest position task dot documents first in the array dot position plus a thousand. Otherwise, we start with a thousand because if there is no highest task at the moment, That means under that status, we are the highest or the first task. So the position will be exclusively used for the Kanban board. Now let's create a new task using await databases.createDocument. Database ID, tasks ID, ID from NodeAppWrite.unique. and then go ahead and add the name, status, workspace ID, project ID, due date, assignee ID, position, new position. Ensure that you have imported ID from Node AppWrite alongside query here. And once you have created the task, you can go ahead and return c.json data task. Great. While we are here, let's also go ahead and let's create the get route. Let's go ahead and let's write a .get method here. Let's add a session middleware. Let's add a z validator, which is going to validate the query. And it will be a z.object. And we have to import zod. So just ensure you have added this. Let me move it at the top here. Inside of this, we're going to have a workspace ID, which will be Z.string. We're going to have project ID, which will be string and knowledge. And we can go ahead and also do the same thing for assignee ID. Then we're going to have status, which will be Z.native and new. of task status from dot dot slash types dot nullish. And we're going to have search, which will be z dot string nullish. And we will have due date z dot string nullish. And then let's go ahead and let's add an asynchronous controller. and inside of here let's go ahead and get the users from await create admin client because we're going to have to populate the users similar to how we handled our member list all right we have prepared this now let's get databases from c.get databases now let's get user from c.get user now let's go ahead and let's extract everything we need from C request valid query. So that will be workspace ID, project ID, status, search, assignee ID, and due date. Now let's go ahead and see whether our member has the necessary rights to do this using the get member. Make sure to await this method. We are passing in the databases, the workspace ID from the query, and the user ID from the user dot dollar sign ID. coming from the session middleware. If there is no member, as always, we can go ahead and simply throw an unauthorized error here. Otherwise, let's go ahead and let's prepare our query. So the query will start very simply by adding an equal workspace ID and workspace ID here, then query order descending $created at and then if we have a project ID, we're going to console.log project ID and project ID so it's easier for you to debug what's going on and let's add a comma here and let's do query.push query.equal So the query here is a constant, but no, you have to distinct between this query and this query. So query.equal project ID, project ID. Let's copy and paste this and let's change this for the status. So status, let's copy and paste this again. This time let's change it for the assignee ID. Let's go ahead and copy and paste this again. This time for due date. So due date and this will be same thing due date and let's copy and paste it for search. This one will use query.search by name and the query here, search. And then we're going to go ahead and actually call the query. So const tasks will be await databases.listDocuments, database ID, task ID, and our query. And then we're going to go ahead and extract all project IDs and all assignee IDs. So const project IDs will be tasks, documents, map, get the individual task, and get the task.projectID. Then we're going to have assignee IDs to be tasks, documents, map, get the individual task, and get task.assigneeID. and then let's populate all projects so projects will be await databases dot list documents go ahead and give them a type of project here from feature project types so i'm just going to move this here i accidentally imported this no need for that so list documents project let's go ahead and give it a database ID, projects ID here. And we're going to do if project IDs.length is larger than zero, we're going to add a query contains $ID project IDs, otherwise an empty array. And you can copy and paste this and do the same thing for the members. And you can remove the type here. So we're going to use member IDs, members ID from config here. Make sure you have added all of these database members, project and tasks. And instead of iterating over project IDs here, we're going to be working over assignee IDs here So this way we are going to populate all of those users Let go ahead and see what wrong with assign Okay so this is assign the array So we are now populating all projects and all members with all the IDs we have gathered from all the tasks matching our query. So now what we can do is we can populate our tasks. So let's create const populated tasks to be tasks, documents, dot map, Get the individual task. And inside of here, first find the project using projects.documents.find. Project.project.$id is equal to task.projectid. And now we populated the project for this task. We can now copy and paste this with the assignee. and we are iterating over assignees like this. It cannot find the name assignees, okay? Oh, I called it members, so assignees. Like this, assignees. And we are looking at an individual assignee here. So assignee $id is equal to task assignee ID. Let me just go ahead and find the proper assignee ID. Make sure you don't misspell the assignee ID here. There we go. So now that we found those two, we're going to go ahead and return the existing values in the task alongside a populated project and a populated assignee. Great. So now let's go ahead and just return c.json data tasks documents populated tasks. There we go. So we now have our get route, which is queryable and which can populate. Oh, we didn't use the users. Let's see if that is my mistake here. Okay. Yes, it is my mistake. we have to use the users from create admin client. So let's go ahead inside of our, we have the populated tasks here. Okay. So we have to go here inside of the assignees here. So these will actually be members. I was corrected the first time. And then we are going to create assignees here by using await promise.all. So we can use async and await inside of dot map. So I'm going to do members.documents.net, asynchronous function holding the member. Let's go ahead and assign users.get member.userID. And then inside return member alongside a populated name from the user and a populated email from the user. Whoops. There we go. Now we have the assignees here. And now you don't have to do dot documents here. You can just directly go to assignees. Now we are ready. Great. So we've finished two very important routes for our tasks. And what we're going to do in the next chapter is we're going to go ahead and turn them into hooks. And we are going to query that data here in the table, for example. and we are going to create the new task model and form. Great, great job. Let's continue by building our create task model. Let's start by going inside of features, tasks, and let's create hooks folder. And inside, we can drag and drop or copy and paste the one from projects called use create project model. Looks like I have just pasted it here. So let me just take that copy from here and add it inside of my tasks here. So I'm going to rename this to use create task model. Let's go inside, ensure that it's inside of the tasks feature folder. And we're going to rename this to use create task model. Let's change this to create task and everything else can stay the same. Now let's go inside of components and let's create our createTaskModel.tsx component. So inside of here, we have to import responsive model. We have to import createTaskForm, which we don't have yet. So let's just export const createTaskModel here. Let's, this structure is open and close to use create task model. Let's go ahead and let's return here a responsive model with an open of is open and on open change to be our set is open. I just need to import this. and let's go ahead and pass it here set is open and inside we're just going to go ahead and create a div to do task form great now let's go ahead and let's play around and find a way to trigger this model but just before we do that ensure that instead of your app folder dashboard layout.tsx you add the create task model. From features, task components, create task model. Looks like I have an error here, but I think that's just a cache. If I refresh, let's see. Create task. So there is something going on here. Create task model here. create task. I'm just not sure what exactly is causing the problem here. Can I go back to my localhost 3000 or will the error still appear? Okay, so the error seems to be persistent here. Let's see what's happening here. Use router, use create task. So something here is causing an issue. Create task. Oh, yes, it's because create task model needs to be used client. Okay. Can I refresh now? There we go. Now it works. So let's go to the new project here. And now on the click of this new button, we're going to open that. So let's go ahead and already copy this. Let's go inside of our task view switcher here. And let's go ahead and add that. So use create task model from dot dot slash hooks. Use create task model. And I'm just going to open. I'm just going to extract the open here. Let's find the button, on click, open. There we go. And we also have to turn the task view switcher into use client. And then you can refresh this and the error should go away. When you click new, there we go. We have our form here. Great. So let's continue working inside of the create task model. And let's start by creating the create task model wrapper. So what exactly is the create task, not the create task model wrapper, but the form wrapper? So the form will have to allow user to select a member and to select projects. In order to do that, we're going to have to prepare those arrays of options for the main form. So we're going to do that in a component, one abstraction above the actual form. So let's create an interface, create task form wrapper props, on cancel. Just an empty void here. Export const create task form wrapper. We'll have to destructure the on cancel from here. On cancel. And let's assign the create task form wrapper props. And then inside of here, we're going to go ahead and we're going to call the use get projects from features, projects, API, use get projects like this. And let's go ahead and also prepare the workspace ID here from use workspace ID. So workspace ID like this. And then we're going to get the projects from data projects. And is loading will be is loading projects. Let's copy and paste this and let's get our use get members from features API use get members. And then this will be members. And this will be is loading members. So let me just double check inside of my use get members here. uh okay i have a bug here i mean a typo failed okay let's consider members server just quickly so inside of my get okay i populated the users i kind of forgot do i do that or not but yes we do that thanks to the model list that we had to do now let's go ahead and let's create the project options here by doing projects, question mark, documents.net, get the individual project and inside immediately return an object with an ID of project.id, name of project.name, image URL of project.image URL. Go ahead and copy this and now you're going to create member options. like this, which will go over members. And it's not going to have image URL, and let's fix this type of image URL if you have it. Then let's define isLoading, which will be either isLoadingMembers or isLoadingProjects. Now, let's go ahead and if isLoading, we're going to go ahead and create the following. So a card from components UI card then card content from components UI card and a loader from Lucid React inside So make sure you have added these two here we going to go ahead and give the loader a class name size 5 animate spin and text muted foreground we're going to give the card content a flex i have a typo here so class name items center justify center and pull height and we're going to give the card pull width a height of 740 pixels border none and shadow none and of course we'll have to return that not just put it in an if clause otherwise it will not render great other than that we're going to go ahead and we're going to render the task form which we don't yet have so let's just write json stringify and let's add our projects here and below that members actually member options and project options like this So now we're going to go inside of the create task model here, and we're going to render create task form wrapper from dot slash create task form wrapper. And we're going to pass an on cancel here to be our close method from use create task model. So close. So there we go. You can see how this loader will be looking like. And then you can see we're going to load the options. So this here is my new project, which I have right here. And inside of here is another one, I believe. There we go, called the test, which is my other project. And these are my options for members. So only one member in this workspace. Great. So that is working. And now we are finally ready to create our create task form. So let's copy one of the forms that we have. So we save some time. I'm going to go inside of features, projects, components, create project form, and I'm going to paste it inside of tasks, components, and I'm going to rename it to create task form. Inside of here, I'm going to go ahead and get the create task schema and use create task, which we don't yet have. So let's quickly do that first. So, okay, you can save this. You leave it broken. That's fine. I want to do the priority thing first. So let's create an API folder here and let's go inside of projects API and let's copy use create project and let's copy use get projects. So one mutation and one query and let's add them here. I'm going to go ahead and rename this one to use create task. And I'm going to go ahead and change this to use create task and I will change the response and the request type to go to tasks. And I will do the same thing here. And it's not going to accept form, but JSON. This will be fail to create task. This will be task created. And this will refetch tasks. And this one, again, an error message. Now let's go inside of the use get project and let's rename that to use get tasks. Let's rename this to use get tasks props. And this will be use get tasks. Let's go ahead and add this. And let's call this tasks. And let's go ahead and call this tasks as well. But we will have some more options here. If you remember, we have all the other queries to add. We can remove the console log here. Okay. So now let's go inside of components, create task form. and now we can import use create task from dot dot slash use create task. Let's rename the create project form here to create task form. So all three instances here, here, and here. Let's rename this to use create task, which we have just added and imported here. Let's go ahead and change these three instances or yes, three instances to create a task schema like this. And now we're going to go ahead and slowly resolve everything we need here. First of all, in the create task form props, we're going to have a non-cancel, but also project options, which will be an array of objects. inside of those objects, we're going to have an ID, which is a string. Let me just see inside of my task from wrapper. Okay. So we store it under ID like that. Okay. Let's be consistent with, we've used dollar sign everywhere. So let's use it here as well, or actually, no, let's not do it. Okay. Because this is our custom, you know, mapper is fine. So just, you know, inside of the create task form wrapper, we are using normal IDs here. So make sure that instead of your create task form, you use normal IDs here as well. ID, name, string, and image URL, string. Below that, we're going to have member options with just these two, like that. Great. So we are going to have that, project options, and member options. Now, inside of the default values, we're just going to prepare the workspace ID and nothing more. Inside of the onSubmit here, we don't need to do any of these things. We're just going to pass in JSON. We're going to spread the existing values and append the workspace ID here. and on success we are just going to do form reset and on cancel or we can do to do redirect to new task instead so no need for okay we're going to use the data here later that's our on submit we are not going to use the handle image change method and inside of here we're going to have create a new task like this oops create a new task Inside of this, we will have a task name, and this will be enter task name. It will control the name property, so that's completely fine. But after that, we will not have the image. So you can remove this entire form field here, like this. And I think that we can already change this to create task, and we should be good to adding this here. So let's go inside of create task form wrapper, And let's simply return create task form here, which we just edited. Let's pass in the onCancel to be onCancel. Let's pass in the project options to be project options. And member options to be member options. All right. Let's see exactly what is not right here. All right. type undefined is not assignable to type. Okay, so member options. Oh, so it's possible that this is undefined. Okay, let's just do this. There we go. And there we go. You can already see, you know, you can now click new here and there we go, create a new task. So now we're going to go ahead and actually implement all the fields we need here. So let's go inside of the create task form and we can remove the input ref no need for that you know need for user ref for image for image icon or for any of the avatar stuff like this and let's go ahead and now continue developing here so so far we have the input for the task name so let's copy this form field and below that we're gonna have a due date here so due date you should also have this you know under these options. So form label will be do date like this. And now we have to create a date picker component, but date picker component does not come from ShadCN. So we're going to have to create it using a couple of components that we already have. So let's go ahead and remove the input here. Let's add it to date picker. Now let's go ahead and let's create the date picker component. So let's quickly do bun add date FNS and wait till you see my version. So 4.1.0. So bun add date FNS at 4.1.0 or NPM install. I highly recommend that you use the exact same versions that I do. Great, we now have that FNS. Let's go inside of components and let's create a date-picker.tsx component here. Let's mark it as use client. Let's add all the imports we need here. So we're going to need everything from React, format from our new date FNS, calendar as calendar icon from Lucid React. We are then going to need CN from Libutils, button and calendar. And we're going to need popover. So all of these are ShadCN components. If you are missing any of those, I already demonstrated a couple of times in the tutorial how to add them. So just run the command Bunex-BuneShadCN at 2.1.0. Add popover or calendar if you're missing one. Now let's go ahead and let's create an interface, date picker props. So the date picker props will have a value of date or undefined on on change, which accepts the value date and an optional class name and an optional placeholder. Now let's go ahead and let's create an export const date picker. And let's go ahead and let's assign the props here. So date picker props like this. And inside of here, we can destructure all of those values that we need. So let's add value, on change, class name, and the placeholder, and let's give it select date as default placeholder. Inside of the date picker, let's go ahead and let's return our popover component, a popover trigger component. Let go ahead and give it an as child property Inside let go ahead and let add a button element Inside let add our calendar icon Let's go ahead and give the calendar icon a class name, MR2, height 4 and width 4. And below that, let's go ahead and use the format from date FNS. If we have the value, format the value in PPP format. Otherwise, simply use the placeholder inside of a span. Regarding our button, let's go ahead and let's give it a variant outline and size large. And then let's go ahead and give it some dynamic classes. So we start by default classes. Pull width, justify start, text left, font normal, and px3. If there is no value, we are going to mute the text. And then we add a class name as the third argument here for some additional styling. Then let's add a popover content here. And inside of the popover content, we are finally going to add our calendar. With mode single, selected prop will be our value, which comes from the props. On select, we'll get the date and call the on change date as date and initial focus. And let's give the popover content a class name of width auto and padding zero, like this. Now this component is ready to be used in the form. Let's go inside of the task form. And let's now add our date picker component from components date picker. And let's spread the field inside. Ensure that you have imported the date picker here from components date picker. I will just move it here. There we go. And now if you go ahead and expand here, you should have a date picker component. And I can select, there we go, 17th of October. Excellent. Now that we have that, let's continue developing the rest of our form. So after due date, let's go ahead and let's add the assignee ID. So I'm going to copy this form field and this will be assignee ID and the text here will be assignee. So the person assigned to this task and in here, we now have to import everything from the select component. So let's go ahead and quickly add that. I'm going to do that after the form. Select, select content, select item, select trigger, and select value. Let's go back to our copied assignee ID here. And let's go ahead and use this. So we're going to start by adding the form item as usual and the form label assignee. And then we are not going to have form control immediately. We're first going to have select. We're going to go ahead and give it a default value of fill.value, whoops, inside of brackets, fill.value, and onValueChange to be filled onChange. Then inside of here, we're going to add formControl, and we're going to go ahead and add a select trigger component here, a select value component with a placeholder, select Assignee, and the self-closing tag. Outside of the select trigger, we're going to have outside of the form control, actually, we're going to have a form message here. And then we're going to have a select content here like this. And then inside of here, we're going to go ahead and iterate over our member options dot map, get the individual member here. Whoops. And inside of here, we're going to render our select item. Let's go ahead and let's give this a key and the value of member dot ID. And we can safely use normal dot IDs here because this is our options, right? It's not coming directly from AppRite. Inside of the select item, we're going to go ahead and create a div with a class name flex, item center, gap X2. And we're going to add our member avatar. We have to import it. So let's give it a class name size 6, a name of member name, and member.name option here. And let's import the member avatar here from features, members, components, member avatar. I'm just going to move it along with the other features. there we go that should be it parameter member implicitly has any type I don't think it does I think this is a mistake let me restart the s server looks like it was a mistake yeah so now you can choose assignee antonio as an option or if someone else joins your workspace you will be able to select that person great so now let's copy and paste this form field and now we're going to create the select option. So we're going to copy and paste this and we will now working with the status here. So this will be status. It will still use select. So that's fine. The placeholder will be select status. Instead of the member options, we're not going to have member options at all here. So let's remove the member options. We're going to have individual select item here with the value task status from dot slash type backlog. And then we're going to simply render out backlog. So import task status from dot dot slash types like this. And now what we're going to do is just copy all of them, right? So we have in progress, in progress, in review, in review, and we have two more to do and done. So task status to do, to do, task status done, done. There we go. So now inside of here, your status, you should have in review, in progress to do, done, backlog. Great. And you can decide on the order of this. Now let's go ahead and let's copy our member select here, our member form field. And let's place it after the status here. And this one will be for the project ID. So this will be the project assigned. And this will be select project. And instead of member options, we're going to use project options. And instead of using member, we're going to use project and project. And instead of member avatar, I will be using project avatar. And we're going to pass in image URL here on project image URL. I think this is image actually. Yes, it's the image prop that we have. Make sure you have imported the project avatar from features. I'm just going to move it along with the other features here. There we go. And now I think this should be enough for you to select a project if needed. Great. And that is it. Those are all the options. So now, well, let's go ahead and try and build this. But I think that we have forgotten to, instead of my tasks collection, to add permissions. So let's go inside of permissions, add users, create, read, update, and delete, and click update here. There we go. Now let's go ahead and refresh this. And at the moment, I have no tasks. I'm going to create a new task. I'm going to select date to be tomorrow. I'm going to select myself as an assignee. I'm going to select the to-do status and a new project. Let's see. Task created. Let's go ahead and refresh this. There we go. workspace ID, name, project ID, S&E ID, description is null, due date is tomorrow, status is to do, and position is 1000. Our API officially works. Great. So what we're going to do next is we're going to go ahead and render these out inside of the data filters here. So we're going to start creating the table, the Kanban, and the calendar board. So for now, what you can do here is actually called the on cancel instead of this. So just on cancel question mark and execute it. There we go. So once you create a new task, I'm going to try again here. Everything should still work just fine. Once it's created, it will close. Great. So we're going to end the chapter here. And now we're going to learn how to render those tasks. Great, great job. Now that we've created a form for creating tasks, we can utilize our useGetTask hook to actually render them on our task view switcher component. So let's revisit our features, tasks, API, and confirm that you have the useGetTasks hook right here. What we're missing here is adding all the necessary props like status, project ID, assignee ID, but we're going to add them in a second. What I want to do now is just load all the tasks in our database. So what we're going to do is we're going to revisit our tasks components, task view switcher component. And inside of here, we're going to add use get tasks from . .api use get tasks. And inside of here, we're going to go ahead and load them. So use get tasks. And right now, we're not going to pass any props inside, but I'm not sure if TypeScript will allow us to do that. So yes, it's requiring at least Workspace ID. And okay, we can pass that. So const Workspace ID, use Workspace ID. From features, Workspaces, hooks, use Workspace ID. And I'm going to separate it here between these two imports. Now go ahead and pass in the workspace ID inside. And this should simply load all tasks in this workspace. So let's go ahead and get our data, which will be tasks and is loading will be is loading tasks. And I will just collapse that like this. So it's a bit more readable. And now inside of here, what I want to do is go inside of each of the tabs content. And for now, all we're going to do is replace the strings with JSON stringify and our tasks inside. So let's copy this inside of the table, inside of the Kanban, and inside of the calendar. So right now, if you save, you should be getting, when you click on table, let's see, there we go. You can see that now we have this huge stringified JSON, which is basically the same data which we recently saw in our database. So what we have to do now\nis the following. First of all, let's go ahead and let's give our tabs a default value because that seems to be missing here. And I want to find a way to control these tabs, which we have. So Kanban, Calendar, and Table via URL so that we can easily share that view with someone else. And for that, we can use a Nux library. So let's go ahead and let's import use query state from Nux. And inside of the task view switcher, let's add view set view. And let's assign that to use query state. And we're going to call it task dash view. And we're going to set the default value to table. And then we can use the view here inside of the tabs. So let's assign the default value to be view. and on value change will be set a view like that. So let's go ahead and refresh this time. There we go. Now you can see the table is selected, but if I click on Kanban, my URL now has a task dash view Kanban. So if I refresh on this page, you can see that Kanban now stays selected. Great. So what I want to do now is go ahead and build the filters, right? So I want to get that out of the way so that we can already start filtering our tasks because the hard part, which was the backend, is easy, is finished for that, right? So we don't have to work on that anymore. We just have to create the UI components for it. So in order to do that, let's go inside of our use get tasks and let's extend the use get tasks props. So we're going to go ahead and add project ID, which will be a string and optional. Status, which will be a string, which will be a type of task status. So our enum from .build slash types. And let's go ahead and do this. Let's add string or null explicitly. And same thing here. So task status or null. And then we're going to have assignee ID, optional, string or null. and due date, optional, string, or null. And now let's go ahead and extract all of those values. So the only one required is the workspace ID. The rest are all for optional filtering. Project ID, status, search, assignee ID, due date. And I forgot to add a search. So let's add search, optional, string, or null. Great. And now when we generate our query key, we want to include all of these optional values here. So if they change, we want to re-trigger this. So let's go ahead and do that. Our query key will be tasks with workspace ID, project ID, status, search, assignee ID, and due date. And then we're going to go ahead and modify our query. So workspace ID will be the standard and then project ID. If we have project ID or undefined. And we're going to do the same for the rest of our fill pairs. So status, assignee ID, search, and due date. And I think that's all of them. So we have the project ID. We have the status, search, assignee ID, due date, and we have workspace ID here. There we go. And you can see that my filters, my use get tasks are still working here normally. So before we go into implementing UI for the filters, let's go ahead and let's first add a loading state here because it just looks weird that nothing's happening for a few seconds here. So we're going to go ahead and we already extracted the is loading tasks here. So now let's use it. Let's go ahead and let's find our dotted separator here after the data filters. And we have already prepared the tabs content inside of the fragment. So what we can do is we can check if it's loading tasks. In that case, we can simply go ahead and create a loader. Otherwise, go ahead and render this fragment. It has to be in a fragment because it needs to have one parent component. Now inside of here, let's give the div a class name of pull width, border, rounded large, height of 200 pixels, flex flex column, items center, and justify center. And then inside of this div, we're going to add a loader from Lucid React and give it a class name of size 5, animate spin, and text muted foreground. So make sure you have added loader from Lucid React. So now when you refresh here, you should have a nice loader here first like this, and then it will showcase the data. Great. Now let's go ahead and let's create the data filters. So let's go and inside of tasks components, let's create a data-filters.tsx. Let's export const data filters. And let's create an interface, data filters, props. And the only prop it's going to accept is hide project filter. So we're going to use this because we're going to reuse the filters on the project page. And it makes no sense that on an individual project ID page, we give the user an option to load other projects. They can use the My Tasks tab for that. So we can go ahead and destructure the data filters props here. So hide project filter. And now inside of here, let's go ahead and let's prepare our projects using use get projects. Let's also prepare the workspace ID from use workspace ID. So both of those come from features. That's fine. And we simply add the workspace ID inside. Below that, we're going to go ahead and extract the members from useGetMembers and again, workspace ID inside. Let's make sure that both of these have their isLoading state and let's remap them to isLoading projects. And the bottom one can be isLoadingMembers. Let's create a unified isLoading constant based on those two values. So isLoadingProjects or isLoadingMembers, both of them are going to change this value the truth. Now let's go ahead and let's use, let's do what we previously did, which is convert these members and projects into options, right? So project options, where we map over documents, get the individual project, and we accept the value, which is $ID, and the label, which is name. And then we do the exact same thing for our members here, like that. And now let's go ahead and do the following. So if is loading for now, let's just return null. Otherwise, we can go ahead and create a div here with a class name flex flex column on a large flex row and gap two right here. And now inside of here, let's also import our components, which we are going to need. So most of this will be a select component. So import, select, select content, select item, select separator, select trigger, and select value from components UI select. But we are also going to use our newly created component called date picker. So let's go ahead and add all of that. But before we do that, I want to go back to task view switcher, and I want to find this text data filters and replace it with the actual data filters component. So make sure you have imported the data filters from .slash data filters. And now you can go ahead and take a look here in this space and see what you are developing. Inside of the data filters, we're going to start with the first item, which is going to be a select component. It's going to have a default value here, which for now can be undefined. We're going to change this later. And let's go ahead and also prepare the on value change, which will just be an arrow function for now. And then let's add a select trigger component here with a class name WFullLG with auto and height of 8. and inside we're going to add a div with a class name flex items center and padding right of two we're then going to add list checks icon from lucid react and let's give it the class name size four and mr of two and let's add a select value with a placeholder all statuses Make sure you have added the list checks icon here from Lucid React. I'm just going to go ahead and move it to the top here. I'm pretty sure that this is a placeholder. Okay, like this. So if you're wondering about this size 4, this is the equivalent of writing H4 and W4. So it's a shorthand operator. You can just write size 4. Okay, and now outside of the select trigger, let's add a select content. And inside, we're going to add first a select item with the value all, and it will say all statuses. And then we're going to add a select separator here and a select item again, but this time with proper values. So we're going to have backlog with the value task status dot backlog. So I'm just import task status from dot dot slash types. And now we have to do the same thing for the rest of the options. So those are the following in progress, in review, to do, and done. And there we go. So now you should have a select here backlog in progress in review to do and done And you can reset back to all statuses Now let go ahead and let create a hook called use task filters So all of these filters like status, project ID, assignee ID, I want them to be preserved in the URL so someone can share this view and share it with other members and see exactly what they have queried. So inside of tasks, inside of hooks, let's go ahead and let's create use task filters dot TS like this. Let's import everything we need from Nux. Parse as string, parse as string enum and use query states. So multiple, not use query state, but use query states. Let's also prepare our task status enum here. And now let's go ahead and let's export our use task filters. So you can just immediately return use query states and open an object inside. And then for each filter, you're going to give them equivalent instructions. Project ID should be parsed as string. Status should be parsed as a string enum and then call object values over task status, which we have imported above. And for the rest, such as assignee ID, search and due date, you can simply use parse as string. Once you've completed this, let's go back inside of our data filters here. And now we're going to use this inside. So just after our member options, we're going to go ahead and use the use task filters, which you can import from dot dot slash hooks use task filters and open an array and then open an object. And inside, you can get the status, assignee ID, project ID, due date, and after. So go ahead and add a comma. So the other part of the array is set filters like this. Now, let's go ahead and also define a constant onStatusChange to accept a value, which is a string. If that value is explicitly all, we are going to set filters. status, and reset it to null. Else, we're going to set filters here. Status, value as task status. Like this. If you prefer avoiding if else, you can use the ternary. So go ahead and set filters, status, if value is all, use null. otherwise value as task status like this. So now we have on status change and we have the status value here. So you can now go ahead and give the first value here a default value of status or undefined. And you can go ahead and select the value here. And call the on status change value like this. So let's try it out. I'm going to go ahead and select backlog. And you can see that I now have a status backlog inside of my URL. So if I refresh this, you can see the backlog stays selected. Same as in review right here. So what we have to do now is we have to ensure that this actually re-triggers and gives us new data inside. So for that, we can copy and paste this use task filters here. And we can go back inside of task view switcher where we load our tasks. So here, I'm going to add them, use task filters, from dot dot slash hooks use task filters. I'm going to remove set filters, no need for them. And then we can assign the status, assignee ID, project ID, and due date. And we can pass them all inside. So workspace ID, project ID, assignee ID, status, and due date. All of them can go inside. So now, there we go. zero documents because none of them are in review. So let's go ahead inside of my database here. I have one task here. It seems two tasks. Let's see. Their statuses are in progress and one is in to do. So if I change the status for in progress, I should load one document and I do. If I change it to in review, I should have zero documents. And if I change to to do, I should have one document again. Great. So now our filters are ready to be wrapped up. So the same way we have created this select component for the status, we're going to do it again for the assignee ID. So let's copy the entire select component here and let's paste it here. And this time we're going to be using the assignee ID or undefined. Let's go ahead and let's develop an on assignee change like this, passing the value. And it's going to be similar. So if value is all, it will be null. Otherwise, it will be value as string. And while we are here, let's also prepare the other ones. And this will not be status. This will be assignee ID like this. So this one will be on project change, project ID, and pretty much the exact same logic here. Great. So we have these two now. So instead of this new one, you're now using onassignee change like this. And now, instead of having this list checks icon, we're going to be using user icon from Lucid React. And the placeholder will be all assignees. So make sure you've imported user icon from Lucid React. Great. And now instead of the select content, we're going to leave the select item here with the value all. And we're going to write all assignees. And after the select separator, we're going to remove this. And instead, we're going to iterate over our member options like this. Dot map member. Make sure you put a question mark here. So .map, member, select item, give them a key of value, a value of value, and a member label inside. Save the file. So we are iterating over this member options here. So we are using value and we store $ID inside. So now if I go ahead and select Antonio, nothing should change because that is the only assignee. Now let's use the exact same method to create our project ID filter. So I'm going to go ahead and just copy the last thing we've created. So this entire select here, paste it here, and we're going to use project ID or undefined on project change. And inside of here, we're going to use a folder icon from Lucid React. So make sure you have added that import here. Select value placeholder will be all projects. and then we're just going to replace the member options question mark dot map with project options question mark dot map select item use the project value as key and value and use the project label inside and let's go ahead and try it out so if i select a new project here let's see if i'm going to get anyone there we go it says one document right here if i select test here zero documents But if I go ahead and create test task, give it a random due date, select myself as an assignee. And if I select the test project and click create task, I actually expect this to refresh, but it didn't. So let's see why. Inside of my use create task. So, I do invalidate the query tasks, but looks like it was not refreshed. Okay. Let's see if I manually refresh here. Is it downloaded or maybe it's not working as intended. So, let's just see what's going on. When I select the project and I select test. Okay. So, we have one project here. Oh, I think this is still working for assignees maybe. Let's see. Let's go inside of data filters here. So we have project options. Okay, so we missed this. This is supposed to be all projects. It's definitely using the project value. Okay, let's see. So if I go inside of my databases here, I should have... Okay. So these two have different project IDs. But these two, so this new task, test task has this project ID. Let me go ahead and copy it. And let me go ahead and find it inside of projects. So none of them are here. That's interesting. Okay. So let's revisit our, which one is it? our create task form. And inside of here, we have project options. So we are using the value as project ID here. Let's revisit our create task form wrapper. Let's see if we are doing anything wrong here. So project options, projects, documents, map, we get the project and we assign project dollar sign ID to the ID here. So this definitely should work. So what I'm going to do now is I'm going to remove all projects that I have, but, you know, just the records. I'm not going to delete any collections. I'm going to go ahead inside of tasks and I'm going to remove all tasks that I have. So let's start from scratch and let's see if maybe something else is here at play. So I'm going to go ahead and, you know, just go inside of my homepage here. I'm going to create a new project, which I'm going to call Project 1, like this. I'm going to create a new task called Task in Project 1. I will select a random date and Project 1. Let's click Create Task. There we go. So this is now refreshed. This is Task in Project 1 And my project ID here is exactly the same as my URL Okay and if I select project one here it works Great. I'm now going to create a project two here, and I'm going to create a new task, task in project two. Whoops. Task in project two, and I'm going to go ahead and select not say knee, task project two, click create a task. task. Now we have two of them. If I select project two, one should be inside. There we go. And while I have some filters, I should also be able to update. So this is project two. So I'm going to do another task in project two. I'm going to give it a random due date. And I'm going to select project two. Again, click create task. And there we go. This gets updated as well. All right. I don't know what that was. Maybe I just had to refresh. Maybe I just had to clear my data. But this is working correctly. Okay, one more filter to add here, which is the exact due date. If we want to filter by the exact due date for that, we're going to go back inside of our data filters component. And we're going to go ahead and create a date picker here. So let's go ahead and do that. after our select let's add a date picker component let's go ahead and give it a placeholder of due date let's give it a class name of height 8 full width and lg without to then let's give it a value due date new date due date or undefined and let's give it an on change to get the date and set filters due date. If we have a date, it will convert the date to ESO string. Otherwise, it will be null. There we go. So now, I'm not sure when did I... Let's see. I should have a task here. Let's see a random due date. So 10th, okay, let's try if I select 10th, maybe zero. Okay, let's try manually. So task in oct 10, october 10. No, it doesn't matter which project it is. And there we go, immediately refreshes. Great. So our filters are working. And what we're going to do now is we're finally going to render these tasks in a nice way. So we have the table component, the Kanban component, and the calendar component. Great, great job. In order to display our newly created tasks, we first have to implement the data table component. So I would highly suggest that you go to ShatZN UI and you go into the components tab and you find the data table component. And now you're going to have to set it up. So we already have data. We already have the normal table component. If you don't, go ahead and select here and add it inside of your terminal. So you can see inside of my source components UI, I already have a table right here. What I don't have is tan stack react table. So let's go ahead and let's install that. So bun add tan stack slash react dash table. and my version is 8.20.5. Once you've installed that, let's go ahead and let's do the following. So you can follow the prerequisites yourself if you want to get familiar with this yourself, but this is what I'm going to do. I'm going to go back inside of my features, tasks, components, And inside, I'm going to create columns.ds. Let's actually make that dsx like this. I'm going to mark these as use client, and I'm going to export const columns, column definition, and we can import column depth from add 10 stack react table. and what's going to be inside is task and it's going to be an array. The issue is we didn't create the task type I believe so let's go ahead and quickly create it. So inside of the tasks folder we already have types but only for Enium task status. So now let's export type task to be models.document and we can import models from node app right and document comes with a capital D like that and let's go ahead and let's assign all the properties. Name, status which is the task status from above, assignee which is a string, project id which is a string, position which is a number and due date which is a string as well. Now we can go back to our components columns and you can import the task type from dot dot slash types like that. And now in here, we define what types of columns we want. So let's go ahead and let's create our first column here using accessor key. And let's map that to read the name property of our task. So let's go ahead now. And I think can just leave it like this. I don't think we even have to do anything else. I mean, for the simplest example, let me see by creating this columns here. Yeah. So, Accessor key and we need header. So, let's add header name like that. Actually, let's make it Task name. Okay. So, I think this And this should be enough just for us to start rendering the task names. And now what you have to do is you have to create a new component called data-table.tsx inside of tasks components. And you have to go to this chat CN documentation page. And then you're going to have to scroll down and go to step two, data table component. And you can go ahead and copy the entire thing from here. In case this changes drastically in the future, you can always visit the source code and copy mine. There we go. So you should have no errors here because we have installed 10Stack React table and we have components UI table. So now, if you go inside of your tasks components task view switcher here, you should be able to go and find the tab content for value table and remove the JSON stringify here and add the actual data table component from dot slash data table. And while you are here, you can also import the columns from .slash columns. And then simply go ahead and add columns here. And add data to be data?documents or an empty array. And the data is actually our remapped to tasks. So let's add tasks.documents like this. Okay, and the reason we have an error here is because we have to go inside of our useGetTasks. More specifically, we have to go inside of tasks server, go inside of our .get method here. And in here, when we run our query, where exactly is our query? There we go. Tasks list documents here. Add the task from the types. So I have imported the task from dot dot slash types alongside the task status. So now inside of here, as you can see, when I go and save this and I go to task view switcher down here, I no longer have the incompatible data. And now if I visit, there we go, task in October 10th. So if I go ahead and clear this now, there we go. You can see all of my tasks right here. Great. So what we can do now is we can go ahead and expand our data table as needed. So let's go back inside of the data table here and let's see what else can we add to it. So I highly suggest that you follow along here as well, because we're going to be copying and pasting some content. So we will be doing this, but we can do that later. Let's go ahead and let's add this row. Actually, no, let's skip that as well, because we need to first create, you know, a reusable component for that. But what we can do is pagination here. So let's go ahead and do this. Prepare your data table and import getPaginationRowModel from TanStack React table. Like this. And then, whoops, go ahead and assign the getPaginationRowModel to getPaginationRowModel inside of the use React table here. So just below getCoreRowModel. and then go ahead and import button. Wrap the entire thing inside of a div one more time. So the entire thing inside of a div like this. You can indent the entire thing if you want to. And then you're going to go to the end here and you're going to copy this highlighted code. and just remove one extra div if you have one. And that's it. That is the pagination built in now. So you can see that we have previous and next pages here now. And if you want to, you can go ahead and change their variant. For example, if they use outline, we can do secondary maybe. Okay, that looks like nothing much has changed. Okay, we can leave it like that. So now what we can also do here is we can implement sorting. So we going to implement the first import everything as React from React inside of our data table here Then import sorting state Then get sorted row model after our pagination one. Then set the sorting state here above the user react table. And then copy and paste these lines here after get pagination row model. So on sorting change set sorting and get sorted row model, get sorted row model, state sorting. And I think that that's it for the data table because now we have to make the header cell sortable. So let's go ahead now inside of our columns. And now we have to do, well, basically this. Oops, let me just scroll up a bit. So arrow up down from Lucid React. Let me add this. And then we will modify our header as this. Let me copy this. There we go. And let's import the button component from components UI button. And let's write task name inside. And I think now by default, this will be sortable. So I can click here and there we go. We can go in reverse or alphabetical order. There we go. Great. Let's see what else can we add. Filtering, that's something that we are definitely going to use. So prepare that data table again. And go ahead and import the column filters state. Go ahead and add get filtered row model. Go ahead and import the input component. Copy the highlighted filtering state and put it below our existing sorting state here. This is how it looks like. The default value is an empty array. And then go ahead and add these two highlighted lines on column filters change and get filtered row model. So we're going to do that just above the state. And then we're going to add column filters here. And then we have to copy the highlighted HTML here and put it in here like this. And let's go ahead and simply do filter tasks. And let's search by name, not by email. There we go. And we can already try this out now here. So if you want to, you can do tasks in, let's see, is this working or not? Name, this should be working. Let me just refresh maybe. there we go so another project too there we go so you can now quickly search for your tasks that being said i really don't like how this filtering here looks and i would rather we you know take our time later and uh implement filtering from server side because this is only filtering what's available here. So if you find that useful, you can leave it here, but I'd rather we have like a search inside of here later. So for now, what I'm going to do is I'm just going to remove this div here, right? We can leave the whole filtering logic. You don't have to remove it. You can just leave it as it is right here. Great. So let's go ahead and let's see, is there anything interesting here that we can add besides that? So we have the visibility here. Well, I'm going to leave it at this. Let's go ahead and do the following. Let's actually modify our tasks so they look like something. So what I'm going to do is I'm going to go back inside of the columns here. And let's go ahead and give all of this a prettier look. So starting with the accessor key name, let's also add a cell here. Let's extract the row. Let's get the name from row original.name And let's return a paragraph rendering the name But let's ensure that it is truncated Or truncate like this Actually, let's use line clamp 1 Okay Now let's go ahead and add another property here Accessor key project Let's go ahead and add a header here. Let's extract the column. Now, we can go ahead and copy this one. Just change this to project. You don't have to change anything else. And now we have to add a cell here. So row. Let's add a comma here so we don't have any errors. and now we have to destructure the project from the row like this and then we have to return a div with a class name plex items center gap x2 text small and font medium like this and inside we can use our project avatar component make sure you have added an import for features project avatar okay and now we have some errors here because we have we haven't passed any images here so class name here will be size six name will be project.name here and this already fixed our issue but let's also add an image to be project.image url so if any project has an image we're going going to load that image. And after that, a paragraph project.name. Let's add a class name, line, clamp one here as well. Great. So now we are loading our projects here. And now I want to copy this entire thing. So both the accessor key header and the cell. So all of it here. and let's paste it below. And now we're going to change it to our assignee here. So we're going to go ahead and change this to be assignee. We're going to get the assignee. So usually this isn't the property inside of project, but remember inside of our tasks server, we populate them, right? So our populated task has the project and the S&E as an object, which has a name, image, and all of those things. That's why we can access them like this. So instead of using project avatar, we're using member avatar component. And let's also change the fallback class name to text extra small. And let's use the S&E here. Let's use the S&E here and no image here. There we go. Now we have the S&E as well. So now let's go ahead and let's just copy the first part. Okay, we can just copy the entire thing. It doesn't matter. Let's paste it. Let's change the accessor key to be due date here. And this will be due date as well. Now we're going to go ahead and get the original due date here. And we're going to return something else. So we're going to return task date with the value of due date here. And our task date will be a cool component, which will warn the user by color how close they are to the due date. So let's go ahead inside of our components and let's create a task dash date dot TSX component here. Let's import difference in days from date FNS and format. Then let's go ahead and let's import our CN library. Then let's go ahead and let's create an interface taskDateProps with a value and an optional class name. Let's go ahead and let's prepare our taskDate. Destructure the value and the class name props here. Inside of here, let's go ahead and let's define three variables. Today being newDate, endDate being newDate and the valuePast, and difference in days being difference in days and date and today, like that. Let's go ahead and let's define the default text color, which will be text muted foreground. And then we're going to create a little bit of a logic here. If difference in days is less or equal to three, we're going to change the text color to red. If the difference in days is less or equal to seven, we're going to change the text color to orange. And if it is less or equal to 14, it will be yellow like this. And then we can go ahead and return a div. Let's see why this is not working. So a div with a class name of text color here. And inside a span with format value PPP. and let's give the span a class name, cn, truncate, and class name like this in case we need to pass some additional properties here and just remove this. And now go back to columns and you can import task date from dot slash task date like this. Let me move the features here above and you can already see how cool this looks. So depending on how close we are to the due date, this is very close, so it's red. This is overdue, so it's red. And this one is far away, so it is yellow. Great. So I think one more field that we have to add here is the status. So let's copy our due date field here. Let's paste it here and let's add the status here. And now we're going to go ahead and change this to be status. And inside of here, we'll be getting the status from row original. And now we will be using our badge component from components UI badge. So this is from ShadCN. So I have this component, right? You can see it right here. But it's missing something. So right now, I can go ahead and render status inside. And this is how that will look like. So backlog, in review, in progress, in progress. That's okay. First things first. I want a method which will transform this text into normal readable text. So again, confirm that.\nWe have badge from component CY badge. We've installed this in the beginning of our project when we set up chat CN. And now what I want to do is I want to go inside of my lib utils where we have the generate invite code and the CN method. And I will export function snake case to title case, which accepts a string. And it returns a string to lowercase. and then it's going to use regex to replace forward slash underscore forward slash g for global and add white space inside. So it's going to replace all underscores with spaces. And then let's replace forward slash. let me just find backward slash b backward slash again w forward slash g get the character character to uppercase there we go now let's go ahead back inside of columns and instead of just rendering status let's use our snake case the title case so make sure you have imported this from lib utils i'm going to move it here let me just separate these two so now it should be more readable there we go backlog in review in progress and in progress and now what we have to do is we have to go inside of our badge component and what i want to do is I want to import task status from features, tasks, types. And alongside this variance here, I'm going to add my own. So I'm going to open an array like this so I can modulary add some key to an object. So task status.todo will be the following. Oops. task status dot to do will be border transparent bg red 400 text primary power background red 400 slash 80 like that and then we have to handle the rest of our states here so go ahead and create an in-progress one, which will have a background yellow and background yellow for 100 slash 80. After that, we're going to have our in-review, which will be blue for 100. After that, we have to create the done status, which will use emerald for 100. And finally, the backlog status, which will use pink for 100. So all of these are using the same classes. The only difference is the color. Now that you've added that, let's go ahead and let's go inside of our columns. And what we can do now safely is give our variant to the status like this because the status is a type of task status and that matches exactly all of this here because we have used up all of them. So now there we go. You can see how cool our statuses are looking now. Great. So one more thing we have to add here, and that is the actions so that we can delete from here so that we can open the edit model so that we can open the individual project page or open the individual task page. Let's go ahead and let's go inside of our features task components here. And we're going to create a new file called task-actions.dsx like this. And what's important here is to import the drop-down menu, drop-down menu content, item, and trigger. And now let's create an interface task action props, which accepts the ID, project ID, and the children. Let's export const task actions. Let's go ahead and assign task actions props here Let's go ahead and let's destructure everything inside So ID, project ID, and children Now what we're going to do here is we're going to return a div With a class name, flex, and justify end And then inside of here we're going to open a drop down menu We're going to give it a model false property and let's give it a closing tag right here. Let's add a drop down menu trigger here and let's give it an as child property and let's put children inside. Now let's go ahead and let's add our drop down menu content with a line end and class name W48. Let's close the drop down menu content. Now let's go ahead and let's add our first drop-down menu item here, which will have the external link icon from Lucid React. So make sure you have added this. Let me just move it here at the top, external icon link. Like this, let's give it a class name of size 4, margin rate of 2, and stroke 2. And let's write task details here. And let's go ahead and give the drop down menu item and click for now to be an empty arrow function. Let's give it a disabled of pulse and let's give it a class name of font medium and padding and pixels. Now let's go ahead and actually put this inside of our columns. So let's go inside of columns here and now we're going to use it. So I'm going to go ahead and add a last ID, which is actions. And a cell here will be destructuring the row immediately. We're going to get the ID from row.original and then $ID. Const project ID will be project ID. Like this. and we are going to return task actions like this from dot slash task actions. And now we're going to pass in the ID to be ID, project ID to be project ID. Oops. And inside, we're going to render a button component with a span. Actually, no need for span, just more vertical from Lucid React. and let's give it a class name size four and let's give the button a variant of ghost and let's give it a class name of size eight and padding zero. Make sure you have imported more vertical from Lucid React. And now when you click here, there we go. You can see how we now have options here. So now let's go back inside of task actions and let's wrap up all the actions which we are going to have here. So we can copy this drop-down menu item and duplicate it. This one will have an icon of pencil icon, and it will be called edit task. Let's go ahead and copy it again. This one will have, again, an external link icon, and it will say Open Project. And the last one here will be a little bit different, having a class name of TextEmber 700, like this, and also FocusTextEmber 700. It's going to have a trash icon from Lucid React. delete task. And I just want to move this drop-down menu item for open project just here after task details. Make sure you have imported external icon, pencil icon, and trash icon from Lucid React here. So now, as you can see, we have task details, open project, edit task, and delete task. We have all of these options right here. Great. So what we're going to do in the next chapter is actually handle this task actions so that they are functional. And once we finish that, we can go ahead and wrap up the Kanban and the calendar. And all I'm seeing here is that these two buttons are technically disabled, but their text seems to be very visible. Let's quickly go inside of our button component. And let's see if that's something I've done wrong here. So we have this disabled background, neutral 100, disabled, disabled. Okay. So disabled, let's give it a text muted foreground. Okay. It did not fix it. So no need to change anything. Okay. We're going to stop here. Great, great job. And what we're going to do next is, well, enable the dropdown actions. let's go ahead and let's implement the delete method from the task actions here we have to start by going inside of our source features tasks server route.ds and inside of here let's go ahead and let's append our new route.delete we're going to use a task id param and add a session middleware. Let's add an asynchronous controller here. And let's extract the user using c.getUser. Let's extract databases from c.getDatabases. And let's extract the task ID from c.requestParam. Then let's load the task using await databases, get document, task. So I have imported the task type here. Pass in the database ID, pass in the tasks ID, and simply the task ID. And then we're going to do const member await, get member, pass in the databases workspace ID which will be task ID here and user ID user If there is no member, we are unauthorized to delete this task. So let's return c.json error, unauthorized 401. And then let's await databases delete document database ID tasks ID collection task ID prop. And let's return the ID of the deleted task inside of data dollar sign ID task dot dollar sign ID. Just like that. And now, it looks like inside of this task type, I forgot to add workspace ID string. So, you can have this typed as well. Great. Now that we have that, let's go inside of API. Let's copy use create task and let's rename it to use delete task. Let's change the constant to use delete task. Let's change the response type here to be tasks. param task ID and then change the method to delete and keep the 200 status code here. Now we have to change this to be just param, change this to be delete and append the task ID param here. This will be failed to delete task, this will be task deleted and this will be failed to delete the task. And inside of here, we're going to destructure the data and we're going to invalidate the query individual task and data $ID. And what we can also prepare already in case we are using a server component to load the individual task page, which we most likely will do, we can already do a router refresh on this. okay great so we have that now let's go inside of our task actions here now let's go ahead and let's import uh use delete task from dot dot slash api use delete task like this. And let's also import use confirm from hooks use confirm. So we have a confirmation that we want to do that. So let's go ahead and let's just prepare the use confirm here. So get the confirm dialogue and confirm method. The title is delete task. This action cannot be undone and the variant is destructive like that. Then let's go ahead and let's destructure mutate any spending from use the lead task right here. And then let's go ahead and let's develop our on delete constant here. Asynchronous await confirm for the OK and if not OK return. And it looks like I'm missing something here. Oh yeah. So we have to pass the program here. My apologies. There we go. So using the id prop here so we now have on delete so we can find the on delete and add it here and we can do the is pending here to disable it and no need for uh disable here or here or here great so let's try this out now i'm going to go ahead and select a random task here click delete task let's try refreshing first. Delete task and looks like it's still not working. Okay, so let's see what exactly is going on. So on click on delete. What's happening is that I forgot to add the confirm dialog. So let's just add it here. And there we go. Let's try again. So I will delete this task, confirm. Task has been deleted. Great. So if you want to, you can avoid the router refresh because it seems to, you know, cause jumpiness, right? So if you comment it out, in that case, when you do delete task, it will just smoothly update it like that. So yeah, if you want to, you can comment it out. I'm going to leave it for now. So now that we have this, let's go ahead and let's finish a couple of other things here. So let me go back to my task actions. And inside of here, I'm just going to prepare my router, const router use a router from next navigation. So make sure you have added this like that. And then what we're going to do is create two methods on open task and on open project. And we need to add the workspace ID param. So let's go ahead and do the following. Let's extract workspace ID from use workspace ID. Hook. Make sure you have added this import here. So these two methods. On open task, we'll go to workspaces, workspace ID, tasks, and then the ID of the task. On open project, we'll go to workspaces, workspace ID, projects, project ID. All from our props here. Let's go ahead and let's assign the on open task here. And let's assign the on open project here like this. So now if I click on open project, it should lead me. Well, I am already here, but let's say this project one. If I click open project, there we go. I'm redirected to project one now. And if I click on task details, this will not work just yet. So we can do that later. All right, so we have this finished. What I want to build next is the editing functionality. That's the only one that's left here. Let's start by going back inside of our tasks server. So source features tasks server route.ts. And inside of here, I'm simply going to copy the existing post method, which we have. like this, and I'm going to append it here. I will then modify this post to be a patch, and I will make it go to slash task ID, like this. I will add the session middleware here, and the JSON here will be create task schema, and we can simply append dot partial to it, actually. Since it's going to be exactly the same, we don't have to worry, right? Now let's go ahead and let's see what do we have to do here. So the first thing we have to do is load the existing task using await databases.getDocument task database ID, tasks ID, and task ID here. Also, we have to destructure the task ID. So let's do that. const task ID from C request param. There we go. And now we can go ahead and assign the workspace ID to be existing task dot workspace ID. There we go. So if we are not a part of the workspace that the task we want to delete is, we will be redirected as unauthorized. And what we have to do now is actually easier. We don't have to do the highest position task. We don't have to do the new position. All we have to do is instead of create document is use update document. And we are simply passing in the existing task ID here. And we are passing in the name status. We are never changing the workspace ID. We can change the project ID, the due date, the assignee ID, and we can change the description. Like this. Let's also destructure the description from here instead of workspace ID. There we go. And we just return back data task. Perfect. So now let's go inside of API. Let's copy, use create task, and let's rename it to use update task. Let's rename this to use update task here. Let's go ahead and get inside of the response type here and change this to be task ID and let's change this to be patch. Leave this at 200. And now inside of here, we're going to get JSON and the param. So let's add the param as well. We're going to change this to be the task ID and this will be the patch method. The error will be failed to update task. This will be task updated, and this will be failed to update task. Let's go ahead and let's destructure the data from the onSuccess so we can invalidate the individual task with data.id here. And since we are currently using server components, let's also prepare router, use router. So I am aware of that flashing that happens every time we do router.refresh. So perhaps later we can go ahead and refactor this so it has a smoother effect, perhaps even completely removing server components if we don't like them. Because we are not using them for anything crucial. It's just for loading mostly static things. Okay, so that's our use update task ready. That's great. What we have to do next is we have to create useUpdateTaskFormWrapper, useUpdateTaskModel, and the controls for the model. Let's go inside of tasks, hooks, and let's copy useCreateTaskModel, and let's rename it to useEditTaskModel. Let's rename this to useEditTaskModel. Let's change this to Edit Task and leave everything else as it is. Now let go ahead and let create the Edit Task model So we going to do this Actually the way I want to use edit task model is a bit differently So I want to simply append the ID of the task inside of my URL. That's how I want to keep the Boolean state on or off. So I'm going to do this. Task ID, set task ID. and I will change all of this to be parse as string instead like this. And then inside of here, I'm going to go ahead and do this on open. We have to select the ID. So set is open ID, meaning set task ID here. And close will be set task ID to null. So then we would have set task ID here, and we would simply have task ID here instead of open. So set task ID, set task ID. Great. And now let's go inside of our components. Let me close everything. And let's copy create task model, and let's name it edit task model. Inside of here, let's rename this to edit task model. like that. And then we're going to change this to use edit task model. We're going to remove the use credit task model and we're going to replace these two with task ID. Then I'm going to go ahead and create a Boolean from the task ID and on open change will simply be close. Then let's go ahead and render this under if task ID exists, only then are you going to go ahead and do this, what you're about to do, like that. So what we have to do now is we have to create an edit task form of wrapper. So let's find our existing create task form wrapper. Let's copy it and let's rename it to edit task form of wrapper. let's rename the props edit task form wrapper like that the one thing that we're going to have here is also the id like this okay and now let's go back inside of the edit task model and let's use the new edit task form wrapper from dot slash edit task form wrapper. And we will have to pass the ID to be task ID here like this. There we go. Now inside of this edit task form wrapper, besides loading the projects and the members, we also need to load the initial values. So let's see, did I add an API for that here or do we need to implement it? We only have use get multiple tasks. So we have to go back to our server route and we have to chain dot get slash task ID param. Let's go ahead and let's add a session middleware here and an asynchronous controller here. And now let's go ahead and let's extract task ID from C request param like that. Let's go ahead and let's get the user from MC get user databases, MC get databases like that. And let's also go ahead and do this. Let's extract users from await create admin client like this. So what we're going to do now is the following. First of all, let's rename ourselves to current user like this. then let's go ahead and get the task using await databases get document as in the task database id tasks id and then task id then we're going to get the current member using those values so current member uses the databases workspace from this task workspace id and the user id right here. As always, if there is no current member, we are throwing an error 401 unauthorized. And now what we have to do is we have to get the project that this task has assigned. So project, and we have to add projects ID here like that. And besides project, we also need to load the member or the assignee that is attached to this task. So what we're doing now is we are preparing to populate those documents. So make sure you have set the project and the member here using the task project ID and task assignee ID. So now that we have those populated, let's go ahead and let's actually load the user behind this member. So user await, users get member, user ID. In case we don't have the user, I think the error will be thrown. So that's okay. Now let's just create a assignee here to be the existing member like that and then name username and email user email and then let's return c.json here data spread the existing task but populate the project and the assignee There we go. Now let's go inside of our API folder. Let's copy use get tasks. Let's paste it. Use get task. Go ahead and change this to be task ID string. So just use get task props. Use get task props. Change this to be task ID. This will be use get task. And simply add task. and task ID next to it. So this is our query key that we are already invalidating when we delete and when we update the task. And then inside of here, we can simplify all of this here. So client API tasks, task ID, get, and we're just going to pass in param task ID like this. fail to fetch individual task will be the error and that's it and we don't need this great now let's go back inside of the edit task form of wrapper here and alongside this we're now going to be loading data initial values is loading is loading task from use get task and pass in task ID to be ID like that and import use get task from dot plus slash use get task just make sure you didn't import tasks right so we are just importing the single use get task which we have just created it only accepts this okay and now let's go ahead and let's change this is loading to also do is loading task we have remapped this to initial values that's okay and what we're going to here is if there is no initial values, return null. We cannot even show this model. And now we have to create the edit task form. So let's go ahead and copy the existing create task form and let's change this to edit task form like this. Let's go inside of here. Let's first just rename the props and the instances here. So this, this, and this edit task form like that. Besides having on cancel project options and member options is going to have the initial values, which is a type of task from dot slash types, which we have already imported right here. Great. And then what we're going to do here is we're going to use the form like this and let's just omit workspace ID true and let's omit description true here as well. And let's go ahead and simply spread the initial values. And let's do due date initial values. Values dot due date will be new date initial values due date. otherwise undefined like this. And we have to destructure the initial values from here. So we have them in the props. Okay. Initial values. There we go. So that's our form here. And now instead of using use create task, we will be doing use update task. So you can remove use create task entirely like this. And now instead of the on submit, we're going to have to do the following. So the JSON can simply accept the values, but the param will have to be task ID initial values dot dollar sign ID like this. There we go. Let's modify this to be edit a task like that. And I think that everything else can stay exactly the same except the last button instead of create, it will be save changes. Let's go back inside of our edit task form wrapper and change this to the edit task form. And we have to pass in the initial values here and make them initial values. There we go. We can now remove create task form here. Now let's go ahead and go inside of our task actions. And what we're going to do here is we are going to add our task ID. Well, actually, we will just destructure this. So use edit task model. This is what I'm trying to do. And let's go ahead and let's destructure open here. Make sure you have use edit task model here. So it's that hook we've just created, which now accepts the direct task ID. So since we have the here in the task action, what we can do here when you ask me to edit the task is I can just go open with ID. And let's see if this will work now. I will refresh this and I will go task in project two. I going to go ahead and select edit task and looks like I have it in my URL but I forgot to render it So let go ahead and find edit task model And let's put it inside of app folder, dashboard layout.tsx. And let's add the edit task model. From features, task components, edit task model. There we go. And I think now, there we go. Already it opened up. Let's try again. So I'm going to go ahead and select task in project 2, edit task. Let's try changing it to task in project 3 and click Save Changes. And there we go. Edited to task in project 3. Let's try changing this from in progress to backlog and to project 1. And let's change the date to 1st of October. Click Save Changes. There we go. Everything works as expected. Amazing. So we've wrapped up the actions here. And what we can do now is we can create the Kanban and the calendar view of our tasks. Amazing, amazing job. Now let's go ahead and let's build the Kanban preview of our tasks. In order to do that, we're going to have to install a drag and drop library. So let's go ahead and let's add bunad at hello-pangia.dnd. And if you want to know the specific version, it's 17.0.0. So I would recommend that you install this under this version like that. So what is this package? This is a fork of React Beautiful drag and drop, which is no longer maintained. So this is the up-to-date version, and they fixed all the leftover bugs from the React beautiful drag and drop, which is currently unmaintained. So now let's go ahead and do this. Let's go inside of our features folder. Let's go inside of tasks, inside of components, and let's create a data Kanban.dsx, like that. and let's go ahead and mark this as, well, actually no need to mark this as use client because it will be used inside of the task view switcher, which is already a use client. So instead what we're going to do here is simply create an interface data kanban props. Let's do that, which will accept the data, which will be a type of task. and let's export const data Kanban and let's return a div data Kanban. And let's go ahead and destructure these. So data Kanban props and destructure the data. Now we can go back inside of our task view switcher component and inside of the tab content, For the value Kanban, we're going to render data Kanban from .slash data Kanban and pass in the data as data. That will be tasks.documents or an empty array. Like that. So the same thing we passed data here in the data table. So now when you click on Kanban, you should see the text data Kanban here. Now let's go ahead and let's actually develop the data Kanban. So we're going to need to import the following things from HelloPangea D&D. Drag drop context, droppable, draggable, and drop result. Besides that, let's also import all of these things from React. So React, use callback, use effect, and use state like this. And now let's go ahead and let's define our initial boards. So we're going to have boards which match the possible statuses of a task. So from the task types, you can also import task status here. And now we're going to create boards for backlog to do in progress in review and done. So let's go ahead and do the following. Let's define const boards to be a type of task status and an array. The first board will be taskstatus.backlog, and then we're going to go ahead and simply add the rest in the available status. So that's the do, in progress, in review, and done. The order matters because that's how they're going to be displayed in here, right in the common board. Now let's define a type tasks state key in task status to be an array of tasks like that. So this is an important type which we are going to need to have. Great. And now that we have that, let's go ahead and let's define the initial tasks. So I'm going to go ahead and set tasks, set tasks to be use state. And I'm going to give it the default of tasks state. and then inside of here, I'm not just going to pass the data. Instead, I'm going to open a method here. We're going to define our initial tasks to be a type of tasks state, and then for each of our task status, we're going to define an empty list, and then we just have to do the same thing for to do, for in progress, for in review, and for done. There we go, like that. And then what we're going to do is we're going to iterate over our data here. So data for each task that we have, initial tasks, task.status.push task. So we are going to push each task into their appropriate column based on their status, like that. And then let's go ahead and sort them by position. So object keys, initial tasks, for each, status, initial tasks, status as task status, sort. We have A and B values and we are very simply going to sort by A dot position minus B dot position like that. So this will now sort them. and finally return initial tasks. There we go. Great. So we now have this done. And what I want to do now is I want to start building our drag and drop here. So let's replace this with a drag drop context. Let's go ahead and give it an on drag end property, which for now will just be an empty arrow function. Inside of here, we're going to add a div with a class name flex overflow x auto. So it's scrollable vertically if we need to. Inside, let's go ahead and let's do boards.map, get the individual board, and inside, go ahead and return a div for each board that we have. And we are going to give a key of board. And we can already add a class name here. Flex 1 and X of 2 background muted padding 1.5 rounded medium and the minimum width of 200 pixels like this. And inside of here, we can simply render out the board like that. And there we go. So you can see how now we have our backlog to do in progress, in review, and done boards ready. So what we're going to do now is we're going to create a component called Kanban column header, which is going to, in a nice way, render the board. So let's go ahead and do that. I'm going to render this as Kanban board, my apologies, Kanban column header, like that. and let's go ahead and prepare the following. We're going to pass in the board to be the current board and the task count will be tasks in that board.length. Now let's go ahead and let's create Kanban column header in the components here. So Kanban-column-header.tsx like this. And inside of here, we're going to create an interface, Kanban-column-header. props with a board task status from types and a task count, which is a number. And now let's go ahead and let's export const Kanban column header here. Let's go ahead and let's assign these props. Now we can extract the board and the task count. And in here, what we can do is we can return a div with a class name px2 py1.5 flex item center and justify between like this. And inside of here, we can add a class name, PlexItemCenter and GapX of 2. And inside of here, we can, for example, add an H2 element with SnakeCaseToTitleCase of our board. So import SnakeCaseToTitleCase, which we've created recently for this exact purpose, but in the data task table. and outside of the h2 element we can add a div which will hold our task count. So this won't look too good at the moment but let's just go back inside of the data Kanban and let's import the Kanban column header from dot slash column header. There we go you can see that now it's a bit more readable as you can see backlog zero tasks to do zero tasks in progress, one task, because I have one task in progress here. So you can see it's already starting to look like something. Great. So now what I want to do is I want to import all the icons we're going to need from Lucid React for our Kanban column header. So let's go ahead and import the following. Circle check icon, circle dashed icon, circle dot dashed icon, circle dot icon, circle icon, and plus icon from Lucid React. And now what we're going to do is we're going to create a status map, more appropriately status icon map, which will have a type of an object, which is called the\nrecord, right? And the key will have to be a type of task status. And then the value will be react.react node. So let's see how that looks. So for example, for task status backlog, we're going to have a circle dashed icon with a class name size 18 pixels, which we have to wrap like this. and I have to fix the typo in the class name. And we're going to have text pink 400. So we are matching the colors which we have in our badge component, right? So backlog has pink 400. Done has emerald. InReview has blue. InProgress has yellow. And to-do has red. So what we can do now is we can go ahead and copy this one and make this to-do. This one will use the circle icon and it's going to use text red. Then let's copy it again, this time for in progress. And this one will use yellow. Let's go ahead and do the next one, which is in review, which uses text blue. And finally, we're going to have done, which uses text emerald like this. And you should have no errors because we used up all of the task statuses here. And now what we can do here is just above our H2 element, render an icon. And we can get the icon using const icon to be status icon map and simply the matching board inside. Because the board is a type of task status and that's what it accepts here. And then you can see that each of our boards here have a nice appropriate icon here. Great. I just forgot to use all the different icons. So I'm using circle icon for all of this. So my to-do will use a circle icon. My in-progress will use a circle dot dashed icon. My in-review will use circle dot icon. And my done will use circle check icon. There we go. So now all of these are used. And there we go. You can see how now all of them have their unique styles. Great. Now let's go ahead and let's wrap up the Kanban column header here. So we're going to go ahead and give our H2 element a class name, text small, font medium, like this. And we're going to give this div a class name, size 5, flex, item center, justify center, rounded MD, background neutral 200, text extra small, text neutral 700, and font medium. So now each of these counts will have its own little box like that. Great. So now inside of the here, outside of this div, let's add a button component from components UI button. I'm going to move it here. We're going to add a plus icon, which we have imported already from Lucid React. And we're going to give the plus icon a class name of size 4. And we're going to give it text neutral 500. And let's give this button an on click of, for now, an empty arrow function, a variant of ghost, size of icon, and a class name size 5. And there we go. We now have a plus icon next to each of our Kanban column headers. So now let's wrap this up by also importing the use create task model hook from dot dot slash hooks use create task model. And we can simply the structure open from here because we are returning it here, right? and then we can simply replace this with open. Let's go ahead and finish this up. And now if I click here, there we go. I have my create a task opened. If you want to play around, you can also, you know, modify the URL a bit more so that it predefines the status. So that if you click plus in in review, it will select in review here. That could be a challenge for you if you want to develop it. So how I would approach that is I would extend the open method to be able to accept, you know, initial status to be a type of task status from types. And then you would have to extend this into an object probably and modify it here as well. and then inside of our, where is it, our task wrapper, create task form wrapper inside of here, we would probably read that, right? Or you can do it inside of the model while you're here, actually. So task model, create task model inside of here, you could probably extract the initial task status and simply pass it through the wrapper and then pass it through the form. So that could be a challenge you can do for yourself. And I advise you do those things. It's always better to customize my projects rather than have them exactly like mine. Great. So that wraps up our Kanban column header. What we're going to do next is we're going to create the rest, meaning the actual drag and drop items. Now let's go ahead and let's wrap up our data Kanban. So we resolve the Kanban column header component. And now we have to render the tasks inside of their columns. So let's go back inside of our data Kanban. It's located inside of the features tasks components. And let's go ahead. And inside of this div, where we render our Kanban column header, just below that, let's render a droppable, which we have imported from Pangea D&D, like this. Let's go ahead and let's give it a droppable ID. And let's make that be individual board, which we have, which is a task status. And now inside of here, what we're going to do is we're going to open a method like this and get the provided prop here. And then we're going to go ahead and return a div. Now for this div, it's important that you spread to give it the appropriate attributes. So let's spread provided.droppableprops. And let's give it a ref of provided.innerref. And now we can give it some class names. So I'm going to give this a class name of minimum height of 200 pixels and padding y of 1.5. And then inside of here, I'm going to render my tasks by board. So tasks inside of this board dot map. And then I'm going to get the task and the index of that task. And we're going to render that inside of a draggable, another component which we have imported from the HelloPangia right here. Now let's give the draggable a key to be task dot dollar sign ID. Draggable ID, which can be exactly the same. and let's give it an index of index. So it needs to be this numeric index right here. And inside of here, we are again going to have a method and a provided like this. And then we can go ahead and finally render the div here. And it's important inside of here that we also do provided.draggableProps, provided dot drag handle props and also a ref to be provided dot inner ref. So make sure that inside of this one you have these three and inside of this one you have these two. Okay, and now inside of here what we can do is we can render a task name for example. So let's see if this will show something and there we go. So I think that already, there we go. I can drag and drop in other columns, but it doesn't stay there. We're going to develop that in a moment. But you should be able to see your tasks in their appropriate columns initially. So now let's go ahead and let's create a component called Kanban card instead of just rendering a task name. So I'm going to create a Kanban card and I'm going to pass in the task as task. So now in the same place where we've created our Kanban column header, we're now going to create a Kanban card. So that's inside of the features tasks components. Let's create a Kanban card.tsx. Let's go ahead and let's create an interface, Kanban card props, to accept a task, which is a type of task from .types. And let's export const Kanban card. Let's go ahead and just assign the props here. let's go ahead and let's destructure the individual task. And we can go ahead and return a div with a class name, background white, padding 2.5, margin bottom of 1.5, rounded, shadow small space Y3. Inside of this, another div with a class name, flex, items center, my apologies, items start, justify between, and cap X of two. And then in here, we're going to render one paragraph rendering the task name. And below that, or more appropriately next to that, we're going to have task actions from dot slash task action. So we're going to reuse the ones which we used in the data table here. And task actions needs to accept a children. So let's give it a more horizontal icon from Lucid React. Let's give this a class name of size 18 pixels. and let's give it a stroke one, shrink zero, text neutral 700, and on hover, opacity 75 and transition. So some effects when we hover on this. So these are the entire class names for more horizontal. Make sure you have added an import for that. And now we're going to have to give task actions a prop ID of task.id and a project ID task dot project ID. There we go. Now let's go ahead and let's style this paragraph here to have a text small and line clamp two. So we're gonna allow two lines before we add three dots as in this text is too long And now let go ahead and let actually import the Kanban card from Kanban card And we can add it here next to the Kanban column header And there we go Now I have my task in a nice little card. And I have these options right here, which as you can see serve to delete a project. But looks like it's not perfectly working. Let's see if I do a hard refresh. Okay, now it's deleted. Okay, I still think this is due to our initial set state of the Kanban. It is going to work smoothly later. And yeah, before we end the project, we are definitely going to take care of that whole router.refresh, which causes this ugly loading state in the middle of our actions, right? So, okay, have at least one or two tasks inside of your Kanban so you can continue working. What we're going to do next is give our Kanban card some more options. So let's go ahead and after we close this div encapsulating the task actions and our title, let's add a dotted separator from components dotted separator. And then let's go ahead and add a div with a class name. Plex items center gap X 1.5. And then let's add a member avatar component, which you can import from features, members, components, member avatar. I'm going to go ahead and give our member avatar a name of task.assignee.name. So this exists, right? Because let's go inside of our task view switcher. So we refresh our knowledge of our data. So we are fetching use get tasks. So individual task is calling this get method right here. And let's see in what way do we get the task. So we load the tasks we need, but we populate each project and each assignee of that task. So then our project, our task has a full project and assignee object inside of them. The only reason why this inside of our, where is my Kanban card here, this is not typed because technically that's not, you know, if I go inside of my types for tasks, we could add, you know, project here to be a type of project. and we could add assignee to be a type of member, right? But we don't even have that, for example. But they are not exactly, I want to have my types stay true to app right definition, right? And in the app right definition, they are not populated. We only populate them locally. Luckily for us, the modules document allows us to have any additional fields that we need. So let's go back inside of the Kanban card. And now you should have the task as a name here. And let's go ahead and give this a fallback class name of text 10 pixels. And now, as you can see here, since my name is Antonio in this account, you can see that I have a little A character here from the person that created this task. Great. So now what I want to do is the following. I want to go ahead and go below the member avatar. and I'm going to render a div, which will be a self-closing tag. And it's just going to represent like a little spacing dot. So size one, rounded full, and BG neutral 300. And then after that, I'm going to add our task date component, which we also used in the data table. And let's go ahead and let's pass in the date. So that is value, task due date. And since we made due date required, it's always going to be here. And let's go ahead and extend it with our class name prop text extra small. So that's inside of our task date why we added an optional class name so that we can modify the font size if needed. And there we go. So now we have the exact same text here and here, right? So we have the same warning inside of different views. Great. We have that. And now let's go outside of this div and let's open up a new div with a class name. Flex item center. Whoops. flex items center and gap x 1.5 and then inside we add a project avatar component which you have to import from features projects components project avatar here and go ahead and give this a name of task.project.name and give this an image of task.project image URL. And now let's go ahead and add a fallback class name here to be text 10 pixels. And after that, a span task project name. And let's go ahead and give this span a class name of text extra small and font medium. And there we go. Now you can see the exact project that this task is a part of as well. Now we have to go back inside of our data Kanban component. So let me close the Kanban card, the task date, my types, just inside of the data Kanban here. After our draggable here, we need to add provided.placeholder like this. so now nothing much should change you should still be able to drag and drop it kind of locks in and then it resets back so we're going to fix that now so okay go ahead and make sure you have provided that placeholder added so after we close the draggable after we close these methods inside of here we have provided dot placeholder and now let's go ahead and let's develop this the on drag and method. So we're going to go ahead and create the const on drag and, and we're going to make this a use callback method. So make sure you have imported use callback here from React. Whoops. Use callback. And let's go ahead and give it an empty dependency array for now. And inside of here, we're going to receive a result, which is a type of drop result. We have already added this import here from Pangea. You can import it as type drop result if you want to be more specific. Let's start with checking if we don't have result.destination. That means this drag and call is invalid and we can immediately break the method. Now let's go ahead and let's destructure the source and the destination from the result. let's get the source status to be source droppable id as task status let's define the destination status to be destination dot droppable id as task status so now we know from where a task is coming from to where it's being dropped let's go ahead and create our updates payload here. Let's go ahead and give it a type of having a dollar sign ID string, a status of task status, and a position of number. So this is what we're going to send to our backend. No need to send the entire new array of tasks or even worse, a matrix of our tasks. So we can just send the updated tasks. So the only updated tasks are going to be the ones whose position has changed or whose status has changed. And we need the ID of each of those as well. So that's going to be an array of those. And we're going to start with an empty array. Now let's go ahead and let's call set tasks. Let's get the previous tasks. Let's set them to be new tasks, starting from the state of previous tasks like this. Now let's go ahead and let's add a comment here so we know what we're doing. Say if we remove the tasks from the source column. So const source column here, we're going to spread new tasks by source status. And then we're going to get the moved task. So inside of square brackets coming from source column dot splice source index by one. Now, in case we cannot find the moved task, we're going to add a comment. You don't need to add comments. So I'm adding comments. So if you read this, you know, you know what's happening. So if there is no moved tasks, which should not happen, but just in case we're going to console an error and we're just going to return what previously existed. So if there is no moved task, we're going to console error, no task found at the source index, and then we're going to return previous tasks. Basically, we are not going to change anything. We are going to assume that this was some edge case where the user, I don't know, somehow left in between drag and drops or did something incorrectly. Great. Otherwise, if we have the move, the task, if we were able to destructure it from the source column, in that case, we can go ahead and create a new task object with potentially updated status. So let's do that by adding const updated task to be if source status is not equal to destination status. So if we change the status in that case, we are going to spread the move task. And so we leave all the properties inside as they are. But we're going to change the status to be the new destination status like this. Otherwise, we are simply going to consider the updated task to be the move the task. So there are two types of moving tasks. We can move tasks. Let me just add one more task so you can see what I'm talking about. Test. I'm going to add a random task. I need in the backlog project too. So let me create one and refresh so you can see what I'm talking about. So we can move tasks in between columns, but we can also move them inside of columns by just changing their position. So that's why we have to handle those cases here. So we have to know, did we change the status or are we just moving it up or down? So if we have potentially updated status we check that here and then we modify the moved task with also a new status Otherwise if the source status is exactly the same we are just considering it to be the moved task with the new position. Now let's add new tasks here from the source status to be source column. What we're doing here is basically just updating the source column. So let's add a comment for that. And then after that, let's go ahead and let's add the task to the new destination column. So we now updated our source column. And now we have to update our destination column. So const destination column will be a copy of new tasks by that destination status. and then we're going to do destination column dot splice destination dot index zero and updated move task updated move the task and i want to call this that so let's call it like that updated moved task like this. Updated moved task. I have it incorrectly named. There we go. So this more closely resembles what it is. So it can be updated, but it can also just be moved. So updated moved task. Okay, we have that. And then let's do new tasks inside of the destination status is now going to have the new destination column like this. Now let's go ahead and let's prepare the minimum update payloads. We're going to do that by simply adding updates payload to be reset to an empty array here. And then we're going to go ahead and push the moved task inside of the payload. So we're going to do updates payload dot push. Dollar sign ID will be updated moved task dot dollar sign ID. status will be the new destination status and position will be math min destination dot index plus one times a thousand and we're going to go ahead and ensure the maximum to be a million above that it's going to break so we're not going to support the infinite amount of tasks these are in our kanban board for that they're going to have the table so we're going to use the same logic we use on our backend, if you remember, task server, when we create a new task inside of here, we define the highest position task and we sort them by thousand. So the new task will be 2000 and then 3000 and then 4000. So in case something gets wrong in our database, we can easily go back and programmatically fix these things by assigning one tasks to be in a half, for example. Basically, by using thousands instead of 1, 2, 3, 4, 5, 6, 7, we allow a lot of room to fix errors if they happen. So we do the same logic here by defining the new position using the destination index. Since index starts from zero, we add a plus one here, and then we multiply by a thousand, and we just use math min to use the lower number. If this happens to be above a million, we are not going to support that change, right? That's way too many tasks for us to handle. So we limit to a million here. Great, we have that. And now let's go ahead and let's do the following. So now we are going to update positions for affected tasks in the destination column. So let's go ahead and do new tasks, destination status for each, get the task and the index. inside of here we're going to check if task and task dollar sign id is not identical to updated moved task dot dollar sign id in that case we're going to do the following const new position will be math min index plus one times a thousand or one million oh and i didn't explain in javascript you can use underscores if you want to. You don't have to. So you can just write it like this. But I find this to be way more readable. When I just see a number like this, I'm way too confused. So this is definitely more readable for me. So this is properly formatted in a normal number. You don't have to worry. So yeah, you can do this, for example. And this is just 1000. Great. So we now have the new position. So we are basically doing the same thing here. So basically, what we're doing now is what you can also do, for example, let's say I have a new task here, to do task. Let me just assign a random thing here. Let me give it a to do and a random project. So what are we doing here now? Basically, besides being able to drag and drop like this, like so we are just changing the position here. In here, we can change the status. But what we can also do is change both the status and the positions inside of that new status. So that's what we have to handle here. So we are updating positions for affected tasks in the new column, the destination column. So that's why we have to do the new position here again. So what we're going to do is if task.position is not equal to the new position, it means we have to update it. So let's do updates payload dot push. ID will be task ID. Status will be destination status. And position will be new position. There we go. Like that. So now what we have to do next is check if the task has been moved between columns. We also have to update the positions in the source column. What does that mean? Well, remember how I just told you that we can do this. We can drag from one column into a different status and we can update this, right? But what if we do this? Now, both the status has been changed and the positions inside of this one and the positions inside of this one have changed. So we have to take care of that as well. So we have to be very careful when doing this. There's a lot of logic happening here. So let's just do if source status is not equal to destination status, meaning that we have change the columns. In that case, new task, source status for each task and index. If we have the task, lowercase task like this, and this is new tasks like that. If we have a task, const new position again will be math min index, oops, index plus one, and then my apologies times a thousand, and then a million. And then we're going to do if task position is not equal or identical to new position in this case, we're going to do updates payload.push id task id. We're going to do status source status. We're going to do position new position. There we go. So I'm pretty sure we now handled every single possible case. And we can now do outside of here, we can do return new tasks. So what this will do is remember, we are doing all of this inside of set tasks. So now we are finally, let me go ahead and close all of this, close all of this. There we go. So after we do all of this ifs, you know, all of this logic here, we simply push the new tasks here. And what's important here is that we have defined the updates payload outside of set tasks. An interesting anecdote I have for you is that while I was building this, I made a mistake and obviously we are now handling this locally. So I think that already, let's try it out. Let's call on drag end so I can continue my explanation here. I'm just going to assign the on drag end here. And I think that already now we should be able to, there we go. We can now drag and drop. And you can see how nicely this is working. And here's a mistake I made. So after we handled this, what we have to do is we have to call an on change method to update our tasks, right? So we have to update this on the back end as well, because right now, if I refresh, everything will go back as it were. And I thought, well, okay, no problem. All I have to do is call the methods every time something changes. So instead of doing this kinds of stuff, updates, payload, push, I would immediately call the on change here or I will call the on change here. Right. That's what I was doing. Or here. I would just do on change. Right. But that's an issue that cannot work because set task, especially inside of its callback, is not exactly. I'm not sure what's the terminology for this, but it can cause mismatch between states. And you can get half of your function being called twice and half of it not being called twice. Basically, what saved me was React strict mode. And I basically noticed that I was getting multiple network requests for each change of my columns here. And while I was debugging, I noticed that this on drag end method right here, when I console logged, you know, fired once. This was true. This only logged once. But then inside of here, I got multiple logs all the time and I couldn't debug why that was happening until I realized that I should not be calling on change inside of the callback of a set state. So that's why we're using this logic where we use the set tasks both to create the payload, which we're going to call on change outside of the set task callback, but also to create new tasks that we finally return inside of this callback. So now that we have that, we can go ahead and call onChange updates payload, but we don't have onChange, I assume. So it's going to be onChange with a capital C and let's add it here in the dependency array. And now what we have to do is we also have to define onChange inside of our interface data Kanban props here. So this will accept tasks, which are going to be an array of objects. And more specifically, these objects inside will take an ID of string, a status of task status, and a position of number. So that's what we are going to send to the backend. And now let's go ahead and let's destructure the onChange here so we get rid of the errors. And there we go. We should now be able to call the onChange here. That's great. But we are not done just yet So another thing that we have here which is not exactly working is this If I go ahead and I click delete task the task is deleted but ignore this ugly refresh We're going to fix that later. Nothing happens here. Why is that? Well, I'll tell you why. It's because of how we initialized the task inside of the data Kanban here. It's because of this, right? So regardless, we have this data is updated. So this data in the next iteration doesn't have that task, which I just deleted. But that doesn't matter because I only use data here initially in my use state when this component has loaded. So what we have to do is we have to implement the use effect to simply repeat what's happening inside of here. Let's do that. After your tasks and the set tasks, add a use effect here. And let's go ahead and give it data as a dependency array. So make sure you have use effect imported. And what we're going to do here is very simple. We're going to start by defining initial tasks like this, but we're going to call this new tasks. And then we're going to go ahead and simply repeat the entire process. So data for each task right here, instead of calling initial tasks, we'll be calling new tasks. So we are basically assigning each task inside of their appropriate column. And then we are ordering by their position right here. So let's do this again. instead of initial tasks, we are calling new tasks here. And then finally set tasks to be new tasks. So now once you save this, let me go ahead and refresh just, you know, to be sure. And if I go ahead and remove this task now, there we go. After that refresh, which we are going to take care of, this has been removed. Now that we have all of that handled, it's time for us to actually call this onChange method. So you can see my task view switcher here is red because I did not pass the onChange method inside. So let's go ahead and simply prepare the onChange method instead of the task view switcher here. I'm going to go ahead and I'm going to go, well, anywhere here. Let's go ahead and let's add it after use get tasks. I'm going to add const on Kanban change to call use callback, which we can import from React. Make sure you have imported use callback from React. I'm just going to move it here to the top like this. And now let's go ahead and inside of here, inside of the parameters, we're going to accept tasks, which are an array of objects which hold $ID, which is a string status, which is a task status from dot dot slash types and position, which is a number. Make sure you have imported the task status from types. Let me just move this here. Okay. And then inside of here, what we have to do is we have to call bulk update, but we don't have bulk update ready yet. So for now, we're just going to go ahead and call on Kanban change here, and we're going to assign it to the on change, the data Kanban. And what you can do inside of here for now is console log tasks. So we can go ahead and try it out. I'm going to open my inspect element here. I'm going to go inside of a random project that I have. I'm going to click on Kanban, and when I drag and drop, there we go. We have tasks right here, which are to be updated. It looks like we have a handy message here from our droppable unsupported nested scroll container detected. I think that might only happen when I collapse like this. I'm not sure if I go ahead and expand like this. Let's see. yeah okay it looks like it's uh it's logging still but i have not noticed any problems i think it's just a warning in case we notice you know some uh unexpected behavior but i think this is warning us in case we are trying to do some weird you know scrollable droppers or something but we're not our is only scrolling like this right so you can see that this works just fine There's absolutely no issue whatsoever. And this will only happen in development, right? This is a development only message. Great. So we now have this. So what we have to build is the actual bulk update now. So let's go inside of our tasks server. So tasks server, which is located inside of features, tasks, server, route.ts. And now inside of here, we have to add bulk update. So let's go all the way down. I want to change that as the last method here. So post, you can also use put, you can use patch. I'm not sure which one is the best for this, but I think for bulk deletes or bulk updates, post is used more often. Let's add a session middleware here. And now let's go ahead and add a Z validator, which will validate JSON, Z.object. We already have Zod important. That's great. So we are going to accept tasks, which will be a z.array of objects, so z.object. And inside, we're going to have an id, which is the string. We're going to have a status, which is z native enum task status. And a position, which is going to be a number, integer, positive, minimum 0, sorry, minimum 1,000, and a maximum of 1,000,000. like this. Okay, and after this, let's go ahead and let's add our asynchronous controller here so that we can finally do the update here. So as usual, let's go ahead and let's get our databases from c.getDatabases. Then let's go ahead and let's destructure the tasks, sumAwaitCRequestValidJSON. So these tasks right here, and there we go. They have the exact same props as needed here. And now we're going to do a check whether all of these tasks have the same workspace ID. So I'm going to do const tasks to update to be await databases list documents, database ID, tasks ID. and I'm going to do query contains $ID tasks.map and get the individual task ID. Task ID. Oops, I misspelled the task here. Individual task like this. Dot ID. So we are loading all, we are now populating all these tasks and this is what we're going to do. const workspace IDs, so multiple, all of them will be new set. Tasks to update, documents, map, get the individual task, and return task workspace ID. Like this. And we can go ahead and assign task here if you want to. So then this will be typed. There we go. So workspace ID. So inside of here now, we're going to do this. If workspace ID's size is not one, in that case, since we're using a set, this is kind of a non-orthodox way to check whether all of these are exactly the same. So if any of this is different, because new set doesn't allow for duplicates, so if they're all the same, the new set will just be one workspace ID. But if any of those are different, it's going to be larger than one, right? So if it's not one, we are going to return an error. All tasks must belong to the same workspace. So we are not allowing this API endpoint to be abused and so that someone can potentially maliciously use it to change a task to a different workspace, right? So all of them need to be from the same workspace because once we have the workspace ID, what we can do then is check whether we have a member or not. So let's also prepare our user here. And then what we're going to do is simply get the individual workspace ID, which can be workspace IDs dot values next value. So that's the common workspace ID, which all of these are supposed to have. And then we're going to get our member using await get member databases, workspace ID and user ID, our user dot dollar sign ID. So that's how we confirm that we can only do the bulk update if we are a member of the workspace in which all of the tasks we are attempting to update are in. Right. So that's why we are doing this kind of a security thing here. So because I mean, for a tutorial, you know, I was considering should haven, you know, handle this. But I do want to show you, you know, some things you should take care of, right? Because this can be abused. This is a very powerful endpoint. It allows for literally any task to be updated. So that's why I want to restrict so that only specific people can call this bulk tasks. We don't want someone else from someone else's workspace to do that. So that's why we have to add a constraint to this bulk update so that they can only be updated inside of a single workspace. And by having a unique workspace, we can do our usual, is this user a member? So if it's not a member, we are not going to allow this user to do any bulk updating. And then we can go ahead and do const updated tasks to be await. promise all tasks dot map gets the asynchronous task so we can use async and await inside of dot map if it's wrapped inside of a promise all usually you can't do that so let's go ahead and the structure from the individual task here an id status and position like this and let's go ahead and let's return databases update document task. Database ID, tasks ID, ID of the current task that we are updating here, and we are simply updating the status and the position here. There we go. And now what we can do is return c.json data update.\nupdated tasks. Great. Now we have the endpoint to handle bulk updates. So now let's go ahead and let's create the API hook for that. So we can use use update task. Let's copy and paste that. And let's rename this to use bulk update, update tasks like this. Use bulk update tasks, use bulk update tasks as a renaming of the constant. And let's fix the response and the request type here. So we are going to be calling tasks and then we're going to be calling bulk update, bulk update. And then inside of here is going to be a post method. And inside of here, make sure you keep the 200 as a positive result. We will not be having any params, just the JSON. And we can change this to bulk update and change this to post. And remove the param from here like this. and we will not have any data like this. Then the toast here will be tasks updated. And instead of here, we're not going to invalidate that. And we can remove the router, refresh from here. We can call this failed to update tasks. Same thing here. And we can remove the router from use router and from next navigation here. And honestly, I'm thinking of doing that everywhere except in my login and logout routes because that refresh is just unbearable to look at. Great. So we now have use bulk update tasks. Let's go inside of our task view switcher component here and let's go ahead and let's add that. So I'm going to go ahead and add it here. Const use bulk update tasks like this. Let's destructure the mutate method. Let's call it bulk update. And let's go ahead and call it here. So bulk update JSON tasks. and let's pass the bulk update here. Okay, so property tasks is missing. Okay, so I have to pass in the tasks as such. Like this. I kind of don't like this. Let's see, if I go inside, okay, let's leave it like this. It's okay. I don't like that I have to say, I don't like that I have to put it inside of the tasks object. So in my bulk update, perhaps I could just give it an array. No need for the tasks object, right? We can just send an array, for example. But let's see. Will we have to change too much then? Yeah, let's leave it like this. Actually, yeah, we have to pass it like this. Yes, this is okay. Yes. So bulk update JSON tasks. and now if we try it out I'm going to go ahead and take a look at my network tab here if I drag and drop this here it calls bulk update and you can see let's see what changed so my payload was to change it to to do and my response was it's in to do right here with the position thousand in that column. And now if I refresh, it stays in the to-do column. And there we go, tasks updated. Let's go ahead and let's create a new task here so we can play around a bit and see if they will update each other's position and things like that. So if I go ahead and add this here, tasks updated, well, they are named exactly the same, So this doesn't make too much sense, right? Let's go ahead and update the task here. Great. So if I drag and drop them one, we need another and refresh. They stay exactly like that. So they have their positions intact. Excellent. It seems like our logic is working flawlessly. Amazing, amazing job. You just finished the Kanban view of our tasks. What's left to do is the calendar view. And then we can go ahead and wrap up by adding some analytics, some home pages, and finally fixing that weird flash which happens, for example, when I delete a task. This is what I'm talking about. Great, great job. Let's continue and let's develop the last view of our tasks, the calendar. So in order to do that, we're going to have to install a package called React Big Calendar. So let's go ahead and do bun at react big calendar and the version I'm using is 1.14.1. Once this is installed, let's go ahead and let's create a data calendar component. So inside of my task switcher here, I'm going to go down here where we have the data content and let's go ahead and let's prepare our data calendar here. It's going to be a self-closing tag, and we can already pass in the data here to be data, my apologies, tasks, question mark, documents, or an empty array. Now in the same components folder, we're going to create a data calendar.tsx like this. Let's go ahead and let's create our interface data calendar props, which are going to accept data, which is an array of tasks. Then let's create an export const data calendar. Let's return a div data calendar. And let's assign our props here. Data. Now let's go back to the task view switcher component and let's import this from .slash data calendar like this. I'm just going to move it here alongside columns data table and data calendar. And there we go. Now you should have data calendar here alongside Kanban and the table. Great. Now let's go ahead and let's develop this. So we're going to have to add some imports from date FNS and some imports from React Big Calendar. So let's go inside of the data calendar here. And let's import all of these from date FNS. Format, get day, parse, start of the week, add months, and sub months. And from React Big Calendar, let's import the calendar and date FNS localizer. And it looks like I don't have the necessary types here for React Big Calendar. So let's go ahead and add that. So my version of React Big Calendar is 1.14.1. So I'm going to add 1.add-d add types slash React Big Calendar. Let me just zoom out so you can see. Oops. Oh, that's my apologies. Let me just pause as I zoom back in. All right, so basically this is the package. Let me show you. Types slash React Big Calendar at 1.14.1. My apologies, this is not the version of the types. So that's our version of React Big Calendar. And our version for the types is going to be 1.8.11, like this. So NPM install this or bun add, and I'm going to add slash D here. So it's saved in my dev dependencies like this. So make sure you're using this version. And now in a few seconds, this should go away. There we go. We can now continue developing our date calendar. So we have the calendar, the date FNS localizer here like that. And let's go ahead and let's prepare some stuff here. So I'm going to add a const locales English US to be NUS, which we have to import from date FNS locale. So let's import NUS from date FNS locale. There we go. So date FNS locale has no exported member NUS. It's because it's US capitalized like this. and same here. Great. And then let's prepare a constant called localizer, which will use the date FNS localizer, passing the format, the parse, start of week, get day and locales. There we go. So now let's go ahead and let's actually create the data calendar things inside here. So I should be able to refresh this and there should be no errors in my calendar view. There we go. So inside of the data calendar here, we're going to do this. We're going to set the value and set value here using use state. So import use state from react. I will just move it here at the top. And the default value here will be the following. So if data.length is more than zero, then we're going to set the date of the calendar to be the first one available in this data. So due date, the first one which we can find new date for. Otherwise, we can just assign today like this. So this will be controlling what month are we currently seeing for our calendar. So if we have some data, it makes sense to load the first month where we actually have some data, right? Otherwise, we can just load today. Now let's go ahead and let's create the events so that we can actually put them inside of the React Big Calendar component. So data.map, let's get the individual task here, and let's return an immediate object. Start will be new date, task due date, and will be new date, task due date. So both of them will be the same. After start and end, we're going to have a title, which will be task name, project, which will be task project. Make sure you don't accidentally pass project ID here. Assignee will be task assignee. And we're going to have status, which will be task status. After that, we're going to have a final ID, which will be task dollar sign ID. Great. Now let's go ahead and let's create our navigation. So const handle navigate will accept an action of previous or next or today. And it if action is previous setValue will be submonths value and one So we are going backwards for one month Else if action is a next we going to set value admons value one Else if action is day let go ahead and let set value to new date. So now we have the handle navigate method. And then inside of here, we can replace this entire thing with a calendar, which is a self-closing tag, and make sure you have imported it from React Big Calendar. So I'm going to go inside of the calendar here, and I'm going to add a localizer to be a localizer. I'm going to add a date to be the current value, which we control from our state. After the date, I'm going to pass the events. Then I'm going to add views and only allow for the month view. So that's the only thing I'm going to allow here. Usually you can switch between day, week, and agenda, but for this project, I'm only going to keep the month view. And let's also add the default view here to be month. Toolbar will be set to true. Show all events will be set to true. Class name will be height full max here will be new date new date dot set full year inside of that method new date get full year executed plus one and formats here are going to be the following weekday format We'll use the date, the culture, and the localizer. And it will return localizer, question mark format, date, triple E, and culture. Or an empty string. And then we can go ahead and leave it like this for now. So let's just save this. And there we go. can see we have something here and we can even see our task right here so let's go ahead and make this uh have some styles we can do that very simply by adding the styles import here so let's go ahead and after we import everything here let's import react big calendar slash lib slash css slash react big calendar dot css and let's save this and let's go ahead and refresh here and there we go we can see something uh right so it's looking a bit better but still not perfect so let's see how we can improve this so we have set the height full here but what i think is missing is inside of my task view switcher here so the place where we render the data calendar here might need to have some special classes around its tabs content. So let's see if adding height full fixes it. Let's go ahead and refresh. Looks like height full did not immediately fix this. So let's continue and let's prepare a padding bottom of four here, just so we have that. And now I just want to double check here my wrapper components here. So let's see what tab list only encapsulates this. So that's fine. So we have this div should encapsulate. Okay, so again, it doesn't encapsulate everything. So it doesn't need to have any height pools here. This div here has height full, flex, flex whole, overflow auto. This one has flex one and width full. Okay, so I think that this should extend to its height, but some things are missing. So here's what I suggest we do. I suggest that we try and extend this ourselves. But just before we do that, I quickly want to go and take a look inside of my app folder, inside of my dashboard, workspaces, workspace ID projects, project ID page right here. And perhaps here I am missing some things before I render the task view switcher. All right, so all of this look okay to me. So what I'm going to do to try and make this calendar take some height is give it some special styles. So inside of the data calendar here, let's focus exclusively on this component now. After we import the styles, we are going to import our custom.data.calendar.css. So data slash calendar CSS and we are going to put it inside of this components file. So data dash calendar dot CSS. That's because this calendar doesn't allow tailwind edits. So let's go ahead and let's add some classes. Let's try dot RBC slash month row and let's give it an apply so we can use tailwind inside of CSS minimum height of 180 pixels. us. And let's see if this will expand it. And there we go. So now I can see that my calendar here has a minimum height and I can see my tasks right here. This navigation is currently not working because we have to plug it in. But since we are already inside of this data calendar CSS, let's go ahead and let's complete it till the end. So I'm going to add an RBC header class and I'm going to apply the following. Text small. I'm going to apply font normal. I'm going to add padding bottom of four. Text muted foreground. And I'm going to add a border bottom of zero. So I think this should modify my header here. You can see that Sunday, Monday, Tuesday now have a missing border here on the bottom. We're going to improve that. And also if you want to, you can add a single apply and then just add, you know, a bunch of stuff here, like font, normal, padding, bottom of four, like you're writing a normal CSS class name, but I just think it's more readable to repeat applies. Now let's add .rbc header plus rbc header. And inside, let's add an apply border left zero. After that, RBC button link is going to have apply text extra small, apply font normal, and apply text muted foreground. Then let's add some other things to our RBC month row. So besides the minimum height, let's give it an apply border top and apply border X. Then let's go ahead and let's do the following. So I'm going to do .RBC month row last of type like this. And if it's last of type, we're going to apply a border bottom. And we are also going to apply a rounded bottom medium. So let's refresh just to ensure that we are seeing the most up-to-date things here. There we go. It's starting to get some shape here. So now we have the last of type and we have applied, if it's the last row, you can already see, we still have this huge border, but I can see a very, very tiny rounded border here inside of this sharp border happening. So this is the doing of that. And now I want to do the same thing here on the top. The problem is CSS doesn't have a first of type, I believe. Or maybe it does have the first of type, but it doesn't exactly work as expected. So what we can do is the following. I found this on, I'm not sure if on Stack Overflow or somewhere else. We can do this trick. not rbc month row, add this, I don't know, I call it the average sign, I'm not sure how you call it, dot rbc month row, and then dot rbc month row. Don't ask me why, but this is the equivalent of first of type, and let's apply rounded top medium. Okay, so there we go. You can see how this corner right here is rounded. So that's what I wanted to do. Okay, we now have this. Now let's add an RBC month view and let's apply border none. And I think this should remove, there we go. So now that big border was removed and you can see how now I have this nice rounded corners only in the first one and only on the bottom one. So that's what these two did. So the last of type is working fine, but the first of type was not working. So I had to do this hack. That's what I'm going to call it because it is a hack. Let's do a .RBCDateCell here and let's apply a padding of two. Let's go ahead and let's add a RBC Today and let's go ahead and give it the background color of inherit. And let's go ahead and let's add an RBC Off Range Background and let's give the background of inherit and we can do both background inherits instead there we go now our calendar is clean here even these off uh these off um what are they called off range backgrounds if you want you know you can leave some of them and then this off ranges you can see will have their own colors if that's what you prefer i'm kind of focusing more on design than practicality for this calendar, you can choose. If you want to specify the today, you can go ahead and remove this and then the today will be highlighted and you can do the same thing for this if you find that very useful. But here's how you can turn it off. Great, so that's it for our custom data calendars. And yes, we have this unknown apply rule. If you want to, you can turn that off in your linter, but it works. It's just a lint warning and it will not stop any build processes. It's just a warning. Okay, now we have to go back inside of our data calendar here. And what I want to do is I want to create some custom components instead of these default ones, which exist, starting with my event card. So I don't want my event card to look as simple as this. So let's go inside of the calendar here. And after the formats, let's add components prop. Inside of here let define the event wrapper component And let go ahead and destructure the event from it And let go ahead and add an event card inside of the event card I going to pass the ID to be event.id. I'm going to pass in the title to be event.title I'm going to pass in the assignee to be event.assignee and I'm going to pass in the project to be event.project. I'm going to pass in the status to be event.status. status. So we don't have to put $ID here because we create events here and we assign ID to be $ID. So we don't have to do the $ID here. Now we have to create the event card. So let's inside of this components folder, create the event-card.tsx like this. Let's go ahead and create an interface here. Event card props. Let's create a title, which is a string, and then just the rest of our things. So I say knee here can be any for now. Let's create a project to be a type of project. Let's add a status to be a type of task status. And let's add an ID to be a type of string. There we go. So make sure you have added the features, project types, and the task status here. And now let's go ahead and let's export const event card. Let's destructure the event card props here. Inside of here, I'm going to extract title, assignee, project, status, and id. And then I'm going to go ahead and return a div with a class name px of two, and then another div inside with a class name. Let's immediately prepare cn from lib utils because this will be dynamic. So I'm going to go ahead and give it some default classes, which will be the following. Let me expand my code screen here. Padding of 1.5, text extra small, background white, text primary, border, rounded medium, border L4, flex, flex call, gap Y 1.5, cursor pointer, hover opacity 75, and transition like this. and then inside of here we're going to add a paragraph title. This is enough for now. Make sure you have added this class name here. I'm going to go over it once again so you can pause and write it out. Let's go inside of the data calendar and let's import the event card from dot slash event card. And now you should be able to see your card right here. There we go. So now what we're going to do is we're going to go inside of the event card and we're going to create a status color map. So let's create a const status color map and let's give it a type of record. For each task status, we're going to produce a string. So for example, if task status is in backlog, we're going to add border left pink 500. So basically the same ones from the batch component. To do is red, in progress is yellow, in review is blue, done is emerald, and backlog is pink. So we are going to match them. So I'm going to go ahead and just wrap this up so you don't have to watch me spell it out. There we go. So to do is red, in progress is yellow, in review is blue, and done is emerald. And all of them have border L. So the only difference are the colors. Great. So now that we have the status color map here, what we can do is we can go ahead and add an extra dynamic class here, which will be status color and then status. And let's see. Oh, status color map. There we go. So now as you can see, this one has a pink border because it's in backlog. If I move it to let's say done, wait for it to be updated, go to calendar, there we go. Now it's green. So this way we have a unified color system so we know exactly what color which status means. Besides having a title here, we're going to add a div below it with a class name of flexItemCenter and gotX of one. And we're going to add a member avatar component here. Our member avatar component will come from featuresMembersComponentsMemberAvatar. And inside of here, we're going to pass in the name to be assignee name. name. And I really ought to create a member type because I really don't like writing any here. But I have explained a couple of times where this comes from. In our get tasks API endpoint, we populate the assignee and the project. So that's why we have this object with the name here. And let's add a div here, which will represent like a little spacing dot. So let's give it a class name of size one rounded full and background neutral 300. And then let's add a project avatar here so we know what project this is a part of. So name will be project name and then an image will be project image URL. So I'm putting question marks here just in case we don't load the project so we don't cause any errors here. And now let me just move this so we get project outer from features, projects, components, project outer. And there we go. We now have a very nice event card right here. What I want to do now is I want to make this toolbar here look a bit prettier. And I also want this on click to redirect to a, I want you to redirect to the actual view of that task. So let's create that on click. So we are finished with the event card. I'm going to add a workspace ID from use workspace ID. I'm going to add a router from use router from next navigation. Make sure you have imported both of those. So we have use workspace ID from features, workspaces, use workspace ID, and use router from next navigation. And then inside of here, we can specify const on click. Let's get the event, which will be a type of react mouse event. HTML div element. And then we're going to stop propagation here. And we are going to do router.push slash workspaces, workspace ID, and then tasks, and then the ID, which we have here. We don't have this page yet. So once we assign this on click to this inner div, so here, not to the top div, it will lead you to a 404 page. So if you click, you will get to a 404. That's okay for now, but later we're going to build it properly. Okay, so we have that ready now. So what I want to do now is build the toolbar component so we can wrap up the calendar view. So let's close the event card and let's go inside of the data calendar. So down here where we specified our components, event wrapper, let's add another component called toolbar, which will render a custom toolbar component. The date will be the value, and on navigate will be handle navigate, like this. If you want to, you can specify it. Well, yeah, let's do that. Let's copy the custom toolbar here. And yeah, did I name this correctly toolbar? Okay, I think this will be fine. Yeah. Oh, my apologies. Not curly brackets here. Normal brackets because we are returning in JSX. There we go. Let's create a custom toolbar now. But we can actually do it inside of here. It's not going to be that complicated. So, and we have a lot of imports ready here. So what I'm going to do is just, you know, before we do export const data calendar, let's do const custom toolbar. Let's go ahead and give it a date and on navigate and let's give it custom toolbar props. Let me just fix the arrow sign here. Let's create the custom toolbar props. It's going to have date, which will be a type of date. and on navigate, which will be action, either previous or next or today. And they will return back a void. And then inside of here, let's go ahead and let's return a div with a class name. flex margin bottom 4 gap x2 items center pull width auto width on large justify center and on lg justify start so we add a little bit of responsivity here now we're going to go ahead and render a button from components ui button so just make sure you have added this import i'm going to move it here. And let's also prepare all the icons which we are going to need from Lucid React. So calendar icon, Chevron left, let's call it Chevron left icon, and Chevron right icon. Yeah, in Lucid React, you can either import like Chevron right, or you can append icon at the end. They do exports of both. So you don't have to do Chevron right as Chevron right icon. You can do it like this as well. Why do they do that? Well, let's say that you want to import the calendar icon. You can see we have a conflict because our component here is named calendar. So usually what you would do is you would rename this as calendar icon. So they obviously know that people do that a lot. So they allowed us to simply import calendar icon like this. So let's be consistent and let's use icon everywhere then. There we go. So these are the icons which we are going to need. Let go back inside of our custom toolbar here And for the first button here I going to give it an on click on navigate previous Variant here will be secondary Size will be icon. Class name will be flex and items center. Inside, I'm going to add a chevron left icon and a class name size four. In between, we're going to have two buttons. So in between them, let's add a div, which will render our calendar icon with a class name size 4 and MR of 2. And it's also going to render, instead of a paragraph format, the current date and in a format of MMMYYY, like this. And let's go ahead and give this paragraph here a class name, text small. And let's give this div a class name. Flex, items, center, border, border input, roundedMD, PX3, PY2, height of 8, justify center, with full, and enlarge with auto. And then let's copy this button and paste it after. this will navigate to next it will have the same attributes here except the class name here no need for the class i don't think there's any need for this class name as well in this button and it will simply use the chevron right icon and there we go you can see how nicely it looks responsive on mobile but when i expand the desktop it's just here and i can now click this and i can go to November. I can go back and I will go to September. There we go. We have finished our calendar component now. So we can now see multiple tasks through table, through Kanban, and through the calendar component. Amazing, amazing job. So what we can do now is we can reuse this task view switcher component and we can put it inside of the my tasks page. So we can end that page. And then what's left is to create the analytics for our individual projects. So analytics will go here above the task switcher. And in the homepage, we're going to have some other analytics. Great, great job. Now that we have finished our task view switcher component, let's go ahead and let's create the my tasks page right here. So we're going to do that inside of the app folder, inside of dashboard, workspaces, inside of the individual workspace ID, let's create a new folder called tasks. And inside of here, let's create a page.tsx. I'm going to go ahead and mark this as tasks page. And since this is a server component by default, I'm going to import get current and I'm going to import redirect from next navigation. And then I'm going to mark this as an asynchronous component. I'm going to await the current user. And if there is no user, I'm going to redirect to slash sign in. There we go. This route is now protected. And now let's go ahead inside and very simply, we can just render the task view switcher like this. So now when you go ahead and click here, it should render the task view switcher there we go and let's just go ahead and give it a wrapper so it's not just returning this i want to give it a div like this and the class name full height flex and flex column like this there we go so now Now it can extend to its full height if needed. Perfect. So we have the tasks page here. We have the settings page. We have the members page right here. So what I want to do now before we go on to creating analytics is the individual task, because that's something we have not done yet. And one thing I also want to fix is right now under my tasks, it makes sense that we allow all of these filters. For example, I can select project two here and it will only load project two. I can select project one and it will tell me no results, right? But if I click on project two, it doesn't make sense that I can change the project one inside of it. So that's why we implemented inside of task view switcher, or maybe we didn't implement. Let's see. I think we added it only in the data filters. We added the hide project filter. So now let's add it in the task view switcher as well. So I'm going to create an interface task view switcher props, optional hide project filter. And let's destructure it from here. So hide project filter. And let's pass it to data filters like that. Now we can go on the individual project ID page. So projects, project ID page, and we can go ahead and pass it here. So let's go ahead and pass down the hide project filter. And we don't have to, this is equivalent to true. So now, if we've done this correctly, let me refresh and see, does this work now? Okay, it still shows the project filter. That's because I don't think we actually use that component, that prop. So inside of Task View Switcher, let's go inside of Data Filters and let's use the Hide Project Filter. So it's unused. Yeah. So what we're going to do is we're going to find our Project ID Select and let's only show it if not Hide Project Filter like this. So make sure you put an exclamation point here and wrap the select around it like this and indent. And there we go. Now it's disappeared for the project page. But if you go into my tasks, you have the project filter. Great. So before we go on to the analytics page, I want to create this, the task details page, which we can access through there or by clicking here, which is currently a 404. But first, let's check whether we have the get task API at all. So inside of features, tasks, server, route.ts, let's see. We have the get for all tasks, but it looks like, oh, we do have get task ID. Great. And let's ensure that inside of here, we do populate the project and the member. That's great. And do I have an API? Use get task. I do. Where do I use use get task? Okay. In edit task form. That's where I use it. I just wanted to confirm. Okay. Now let's go ahead and let's go inside of source app folder, dashboard, workspaces, inside of workspace ID, inside of tasks. And in here, we can now create a new folder, task ID. And inside, we can create a page.dsx. Let's go ahead and export const, or let's just do export default task ID page, div task ID. And now what I'm going to do is mark this as an asynchronous component. I'm going to get the user using await get current. If there is no user, I will return no. I will redirect my apologies to slash sign in. There we go. So we resolved that. And now usually what I do is I create a query to get the task. But we've noticed that in order to update server components, which use those queries, what we need to do is router.refresh, which really doesn't have nice user experience. So I'm going to introduce a new structure now. And then later, we're going to refactor all of those places where we use this asynchronous components as server components and load the initial data as well, because it really, really don't like that structure. But we're going to do that later. And we can start now. So I'm going to go ahead and have alongside my page inside of task ID, I will simply create a client.dsx. And we know what this means. This will simply be a use client component. Let's go ahead and let's export const, well, let's call it task ID client like this. And let's return task ID, task ID client. Great. Like this. And let's go back inside of here. And we are very simply just going to return task ID client inside like that. So we can leave this to be responsible, you know, for the redirect. And then if you want to, you can pass from the params, the task ID, or we can just create a hook and use params inside. And that's what I'm going to do. So I'm going to go inside of my features folder. Where are my features folder? Here, inside of tasks, inside of hooks. And I will simply create a use task ID.ts file. And inside of here, I'm just going to import use params from next navigation. export const use task id define the params and search for params dot task id so basically i am looking for the same thing that i have inside of my dashboard workspaces workspace id tasks task id right here make sure that the capitalization is correct and now let's go inside of client and now i can get that here's a const task id here will be use task id now that i have use task id what i can do is I can fetch my data. So I'm going to go ahead and get data and it's loading here from use get task and I will pass in the task ID. There we go. And now in case I am loading, I can go ahead and create a something called a page loader. So if it's loading, I can return a page loader. So the reason I'm doing this different kind of structure now is because I want to standardize it So I can then repeat it elsewhere where we previously used the queries. I will most likely delete all queries because I really don't like using them. They are good for preloading, right? But since I didn't implement that kind of use query at all, it doesn't make sense to force server components where they're not useful. For example, for redirecting, I find them very useful because there's no need for use effect, right? I find that great, especially inside of my\nworkspaces page.tsx. So especially this part, right? I can already see this in use effect will just be a mess, right? But here it's pretty straightforward. So I'm going to leave queries only, you know, for this kind of things. But for the rest, I'm just going to go ahead and do things like this. So let's go ahead and create this reusable component, page loader.tsx. And it's going to be like this. So import loader from Lucid React, export const page loader, give it a div with a class name of flex, item center, justify center, and a height pool, and a loader component from Lucid React with a size 6, animate spin, and text muted foreground, like that. And while we are here, let's also create a page error inside of components. So page error.dsx right here. So import alert triangle from Lucid React. Define the interface page error props right here. The message is a string. Now we have a export const page error. The message by default will be something went wrong, but it's going to be a prop. So it allows you to specify a custom message if needed. And there we go. You can see the whole class names here. So again, flex, but this time flex column, items center, justify center, hide full, alert triangle icon. Pretty much the same as in page loader with the exception of removing the animate spin. So make sure you remove that. And then a paragraph text small font medium text muted foreground with our message. Great. Now that we have the page error and the page loader, let's go inside of the client here and let's import the components form components page loader like this. And if there is no data, we're going to return page error with a message task not found. And let's import this from components page error like that. Otherwise, we can go ahead and JSON stringify the data. And there we go. So now when you refresh this, there we can see the loader. And then it's transformed into this. I like this feeling of spas, single page applications, way more than I do server side components. That being said, they are handy, you know, for things like this, they're amazing. But for handling this kind of stuff, I don't know, I just prefer it like this, especially with React Query. Great. So we have our task ID client here. So now what I'm going to do is I'm going to style this. Let's go ahead and turn this into a div. Let's give this a class name of flex, flex column. And inside of here, let's create task breadcrumbs like this. And let's pass in the project to be data project and the task to be data. Now let's go ahead and let's implement the task breadcrumbs inside of my, let's go inside of features, tasks, components. So let's add task breadcrumbs.csx component. The task breadcrumbs will have an interface which accepts the project from features, project types and task from dot dot slash types like this and let's export cons task breadcrumbs let's go ahead and let's assign the props here so we can destructure the project and the task and then let's open an arrow function here and let's go ahead and do the following let's create a div with a class name, flexItemsCenter and gapX2. Let's render the projectAvatar component. Let's give it a name of project.name. Let's give it an image of project.imageURL and a class name size6 and largesize8. After that, let's go ahead and let's create a link from next link. Let's give it an href slash workspaces. And then we need a workspace ID. So let's prepare const workspace ID. We use workspace ID. So ensure that you're having all of these imports here as I do. Project avatar from components project avatar. And this from workspaces hooks use workspace ID. And so we are adding workspace ID here. After that projects. and after that, project $ID. And inside of here, we're going to add a paragraph which renders the project name. And give this a class name of text small, lg text large, font semi bold, text muted foreground, hover opacity 75, and transition. like this. Then let's go ahead and let's add a Chevron write icon from Lucid React. So make sure you have added this import here. And let's give the Chevron write icon a class name of size four, LG size five, and text muted foreground. Let's go ahead and add a paragraph here, task.name with a class name of text small lg text large and font semi bold and then let's add a button component from components ui button which will have a class name ml auto so it's in the opposite corner of these let's give it a variant of destructive a size of small and let's go ahead and write a trash icon inside from lucid react and the span delete task make sure you have added the trash icon here and give the trash icon a class name of size 4 and on the large mr of 2 because this span will be hidden by default and only visible on large like this and i forgot to return this. So make sure you wrap this in return. So you don't have the mistake as I do. Let me just indent it. There we go. Let me move this here. And now let's go ahead and let's go inside of client.tsx and let's import task breadcrumbs from features, task components, task breadcrumbs. And there we go. Now you can see that you will have the individual task here and you can delete the task from here. And when you click here, you will be redirected to the project page. And then you can click here, task details. And there we go. You are now seeing the breadcrumbs of that task and you will have a quick delete button here. But on mobile, this button will turn into just a small button like that. So let's wrap up the task breadcrumbs now by introducing the useDeleteTask and the confirmDialog, which luckily for us, we already have all of those. So this will be a mutate method here. UseDeleteTask like this. And then we are going to have confirmDialog and confirm method from useConfirm from hooks. UseConfirm, so I'm just going to move this here. so inside of the use confirm go ahead and give it a delete task this action cannot be undone and destructive variant like this and let's create a const handle delete task to be an asynchronous method which will check for await using the confirm if not okay we will break the method otherwise let's go and delete the task by passing in the param to be task id task dot dollar sign ID. Is it expected zero arguments, but got one? Oh, not usually. That's my apologies. Mutate. There we go. And what we're going to do on success here is we're going to go ahead and redirect the user away from here back to the tasks page. So let's go ahead and let's add a router from use router from next navigation. So make sure you have added this import here. Use router from next navigation. And we're simply going to do router.push slash workspaces workspace ID slash tasks. And now let's go ahead and add the handle delete to the button here on click handle delete and we can disable it if it's pending. Let's go ahead and fix a disabled prop and let's extract a spending here from use delete task mutation. And let's render the confirm dialog somewhere like this. And now when you click delete task, you have a confirmation. When you click confirm, you are redirected to the tasks page. Great. So now we have to create a new task here. So new task. Let's select the due date. Let's select an assignee, a status and project. Let's create a new task. And now let's go back inside of the task details here. There we go. So now we're going to go ahead and add the overview of the task here and then a description, which will be modifiable by users to add more information about the tasks. So none of these are going to be too difficult because we have most of these things implemented already. So let's go back inside of the app folder, dashboard, workspaces, tasks, task ID, client. the SX. And alongside having the breadcrumbs here, we're now going to add a dotted separator component from components dotted separator. And I should reverse these like that. We are now also going to give the dotted separator a class name MY of 6. And then we're going to add a div class name grid grid columns one, but on large grid columns two and gap four between them. And then inside of here, we're going to have a task overview and pass in the task to be data. So now we have to create the task overview component. So let's go ahead and do that. So in the same place where we created our task breadcrumbs, so the tasks components, let's create the task overview component. .tsx. Let create an interface task overview props which will accept a task which is a type of task Let export const task overview here Let destructure the props And let's go ahead and let's get the task from here. And then inside of here, let's go ahead and let's return this. And let's add a div with a class name, flex, flex, colon, gap, y4, and call span of one, because we are using this inside of a grid. Let's go back to the client and let's quickly import the task overview so we don't have these errors anymore. Okay, and now we will be able to see what we are developing. Let's add another div inside with a class name of background muted, rounded large, and padding of four. Now inside of here, I'm going to add a paragraph overview, and I will add a class name here, text large, and font semi-bold. like this. And then let's go ahead and let's add a button component from components UI button. Let's add a pencil icon from Lucid React. So I'm going to move this here and I will separate these like that. This will have a pencil icon. The button will have a size of small, the variant of secondary, and this will have a class name of size of four and MR of two, because it's also going to have an edit label like this. Now, what we have to do here is ensure that this is inside of a div. So these two should be inside of another div like this and give it a class name of flexItemsCenter and justifyBetween. There we go. Now it looks better. And now we're going to go ahead and add a dotted separator here. So import that from components UI dotted separator. Let's give the dotted separator a class name of MY4 like this. And then let's create a new div. And let's add a class name, flex, flex, column, and gap, Y4. And what we're going to do now is create one more component, which we are now going to reuse to nicely display some things here so we don't have to copy and paste a lot of things. So inside of the same folder components, Let's create a new file called overview property dot TSX like this. And let's go ahead and let's create an interface. Overview property props will have a label and children. So let's go ahead and let's define that here. So I will just collapse this so you can see. There we go. Overview property, overview property props. and then inside of here we are going to return a div with a class name flex items start and gap x of two then another div with a class name minimum width 100 pixels and then we're going to have a paragraph rendering the label and then a class name inside of the paragraph here text small and text muted foreground. And then we're going to have a div rendering the children inside. After that, another class name, flex items center and gap X of two. Great. So we now have this reusable component. We can go back inside of the task overview now, and we can add as many of them as we want. So let's add an overview property here. make sure you have imported overview property from .slash overview property we're going to give the first one a label of assignee and then what we're going to do is we're going to add a member avatar as the children so make sure you have imported the member avatar from features members components member avatar and I'm going to go ahead and give this a name of task.assignee.name like this. And I'm also going to give this a class name of size 6. There we go. So you can already see the assignee and the member avatar. And then a paragraph task assignee name again here. But let's also give it a class name here, which will be text small and font medium. Then let's add another overview property here, this time with a label of due date. And inside of here, we're going to go ahead and render the task date component. From dot slash task date, like this, let's go ahead and give it a value of task due date. And let's give it a class name of text small and font medium. There we go. Now, let's go ahead and repeat our overview property one more time. And let's give it a label of status. Let's give it a badge from components UI badge. So make sure you have added this import here. Let me just move the features here above. Now that we have the badge, remember, they have all the necessary variants here for our status. So we can safely do the same thing we did in the columns of the data table. Add snake case to title case to task.status and passing the variant here to be task.status. And there we go. In progress, yellow. And make sure you have imported snake case to title case here. There we go. So that is our overview tab here. And now here we're going to implement the description. So what we have to do to wrap it up is just enable the edit button, which will be quite simple. We have to import the use edit task model. So open useEditTaskModel from . . . hooks useEditTaskModel. And on click of the edit button, we are going to call the open. And we have to pass in the task.$ID. And now when you click edit, it will load that task. And you can change this new task 1 to 3. Click Save Changes. Oh, and this is supposed to close. And I think it did close. I think it's just my hot reload. That was messed up. Let's see. Let's refresh again. New task one, two, three. Let's change this from in progress to backlog, for example. So backlog, save. There we go. Change to backlog. Why did it update immediately? Because in my use update task, I invalidated the queries right here. Task data ID. right? It's because of that. And we store our use get task with that query key. So you have to be careful that you're using the proper query keys here. Great. So now that we have this finished, let's go back inside of the clients. And now we have to implement the last thing on this page, which is task description. And it's not going to be awfully difficult. We already have the update method. We just have to implement the component. So what we're going to do is implement the task description in the same place. Features, task components, and let's create the task description dot DSX. And now let's go ahead and let's simply create an interface task description props. Let's import task from types. Let's prepare some other imports which we are going to need. use state from React, pencil icon, and x icon from Lucid React. We're going to need some components from ShatCN, button, text area, and dotted separator. In case any of these are missing for you, you have to install them with ShatCN. So let me show you once again, one ShatCN 2.1.0 add text area, for example. Great. And let's also add our API use update task from .NET slash API. Use update task. Great. Now let's go ahead and let's export const task description here. Let's add the task, task description props. And then let's go ahead and let's create the UI. So return div with a class name, which is going to be padding for border and rounded large. And now let's go inside of the client and let's just import task description so we can already start seeing the changes here. So I will just add it here. And there we go. I can see an empty box here. So now we're going to continue to develop inside. Let's add a div with a class name, flex items center and justify between. Inside of here, I'm going to add a paragraph overview. And I'm going to add a class name, text large and font semi-bold. And then I'm going to add a button component. And I'm going to do the pencil icon inside, like this, with a class name of size 4 and MR of 2. And I will say edit like this. And let's give the button a size of small and a variant of secondary like this. And then outside of this div, let's add a dotted separator. Let's give the dotted separator a class name of MY4. and let's go ahead and give this a div with a class name flex flex column and gap y of four like this and then inside let's render a div with task.description which I think it's missing from my definition here yeah so let's go inside of features tasks types and let's add the description as an optional string. So if we have task description, we're going to render that, or we're going to render a span, no description set, and give it a class name, text muted foreground. So there we go. This now says no description set. So what I'm going to do now is the following. I'm going to add the is editing prop, is editing set is editing, useStatePulse I going to add value and set value from useStateTaskDescription I will define mutate and is pending from useUpdateTask method I will just separate them from the states like this And I will add const handle save method here update task And I will pass in JSON to be an object and pass in the description here. And then I will also add my params and, of course, mutate. Let's call the param here with task ID being my task dot dollar sign ID. And description will be value. Great. We have that. We have a spending. And now let's create different states depending on whether we are editing or not. So first of all, let's start with this button here. So I'm going to create an on click here, which we'll call set is editing. and depending on its previous value, we're going to do a toggle by simply doing the reverse of what was previously inside. So if it was false, now it's going to be true. And then depending on that, we're going to render one thing or another. So if we are editing, we're going to render one set of icons and text, otherwise another one. So let's do it like this. and inside of here let's add an x icon with a class name size4mrf2 and then inside of here if it's editing it will be cancel otherwise it will be edit so now if you click edit uh it's editing it should change to cancel so let's see why did not oh i have to save there we go now it says cancel and edit and you can toggle great now we have to do the same thing here but just show the text area instead so i'm gonna go ahead inside of here like this um and this is how i'm gonna do this this div here can actually be outside it doesn't have to be inside of the flex flex column so let's do this if is editing we're going to render that div and we're going to put text area and the confirm button inside otherwise simply render this div like this inside of here we're going to add a text area component we're going to give it a placeholder of add a description we're going to give it a value of value. We're going to give it a rows of four. We're going to give it an on change to take the events and simply set the value to the event target value. And we're going to go ahead and give it a disabled if is pending. And below that, we're going to add a button component. It's not going to be a self-closing button. So give it a close here. And it's going to say if is pending, it will be saving. Otherwise, save changes. Let's give the button inside a size of small. Let's give it a class name. WfitMLauto. And let's give it an onclick to be handle save. And let's give it a disabled of is pending. So now, if you go ahead and click edit and change the description and click save changes. It should work just fine. Task updated and there we go. The description is set. I can even do a hard refresh here and it should work just fine. There we go. Change the description. Amazing, amazing job. You have just completed the individual task view. So what we're going to do next is we are going to go ahead and create further develop the project pages here and the home pages here. What I want to build next is go back to the project ID page right here. And there's a couple of things I want to do. First one is I want to refactor my server component usage inside of here. So if you go inside of source, app folder, dashboard, workspaces, workspace ID, projects, project ID, inside of here, I'm using the await get project from project queries. I don't want to do that because in order to update this page, especially the name and the image, I would have to do the router.refresh. So I'm not going to rely on that. Instead, what I'm going to do is what I did with the task ID page. So I'm going to create a client.tsx inside of here, and I'm going to mark it as use client. I will export const page, my apologies, project ID client here. And no need to add any props inside. What I'm going to do is this entire thing. So I'm just going to pass this. So let's return. There we go. Let's import project avatar here. Let's import the button component from components UI button. Let's import link from next link. Let's import pencil icon from lucid react. And let's import the task view switcher. So we should only have these initial values causing us errors. I'm now going to move this to here at the top, and I'm going to prioritize the features above the components. Now that we have that, what we're going to do is go back inside of here, and we are very simply just going to render the client. So this will be called project ID client. And now we don't have to do this anymore. So we can remove get project, project avatar, task switcher, button, pencil, link, and we can also remove the project ID page props. No need for them as well. We will only handle the authentication here. Now let's go inside of the project ID client and let's go ahead and let's get the project ID from use project ID. It looks like we don't have that one. So let's quickly create it. So it will work the same way our task ID or workspaces ID hook works. So use task ID. Let's copy that. Let's go inside of projects, hooks, and let's add it here. Let's go ahead and let's rename this to use project ID. Use project ID. And this will be project ID with capital I. Now let's go back inside of the project client. And we can import the use project ID from features, hooks, use project ID. And now we can go ahead and call the use get project. Looks like we only have use get projects. So we have to add that. We didn't add it because we just use a server component, but it's good to have API for that as well. So first, let's confirm. Inside of API, we don't have use get individual project. But do we have inside of server route.ts, do we have the individual get project ID? So let's see, get all, not this. We have post. So we don't have get individual project. Let's implement that. So I'm going to add get, and then I'm going to add slash project ID. I will add a session middleware and an asynchronous controller here. I'm now going to extract my user from c.get user. I will extract the databases from c.getDatabases, and I will destructure the project ID from c.requestParam. Now I'm going to load the project. So project will be awaitDatabases.getDocument, like this, with the project type. database ID, tasks ID, and project ID from our request param. And then we're going to get the member using await get member. Databases, workspace ID will be existing project or simply called project.workspace ID. And then we're going to have our user ID come from user.$ID. If we don't have the member, we are going to throw an error here. Otherwise, we can go ahead and simply return the project inside of data. Now that we have our get project ID, we can go inside of projects API. And let's copy use get projects. Let's paste it here. Use get project like this. Make sure it doesn't have S at the end. Let's rename this to use get project and this to use get project and this will look for project ID and this will be use get project props and project ID here. And this will be the individual project and then project ID. And the query here will be just a project ID and it won't be a query. It will be a param because we are looking for projects and then looking for the project ID. There we go. So failed to fetch the individual project. No need for any console logs here. There we go. Use get project is now here. And we can now go back to our client for the project ID. And we can import use get project. So individual use get project. And I will remove the falsely imported use get projects one. And then we just pass in the project ID here. And now inside of here, we're going to have data and is loading. And we can do the usual thing. If it's loading, we're just going to go ahead and return a page loader component. And if there is no data, we're going to return a page error with a message project not found. And then we're going to replace all the initial values usage with data. There we go. And we should get rid of any errors now. So I'm just going to move this alongside here. Let me just sign it like that. And let's move that here. And let's double check. Can we load this or not? Do we get any errors here? Let's see. It's loading a project. And it looks like I'm getting a 500 error because it says document with the requested ID could not be found. So let's see what exactly are we sending that's wrong. Inside of use project ID, we are looking for params project ID. We are sending the project ID. That's for certain. And let go instead of client there seems to be an error here Oh there an error inside of here Project ID client So we also have to replace this with data ID but that is not the reason we are getting that error. But yes, fix this as well. So that's inside of the project ID client. So what's happening here is that we are failing to load use get project. So let's go back inside of our route here. project ID, project ID from param, project. And this is, I think, where something goes wrong. Oh, because I'm loading tasks ID. Projects. My apologies. We don't need tasks ID here at all. I was so used to working with the tasks. I forgot inside of the get project ID, we have to use the projects ID collection. And now we should be able to load our project. There we go. And what's cool now is that when I click edit project here, and if I go one, two, three here, and if I go back, there we go. You can see how this nicely updated because of React query, right? So while we are here, let's also fix the project ID settings page, right? Since we just implemented this API endpoint, I just want to fix those things. So we have to find the project ID settings page, and I want to resolve it. Let's go inside of source, app folder, standalone, workspaces, workspace ID, projects, project ID, settings. And inside of here, I'm going to remove these initial values here completely. So I will not be needing this props here at all. So let me remove this. Let me remove this. Get project. And I'm going to go ahead and remove this as well. So the only thing I'm going to do is create a client.dsx here. Mark it as use client. And I will export const project ID settings client like this. And inside of here, well, what I can do is I can return this as well. So let me add this and let me add an edit task form here. my apologies edit project form and remove the edit task form here and what i'm going to do here is i'm going to get the data remapped to initial values from use get project and we can get the project id here from use project id and we can simply pass in the project id there we go this is way more consistent if you ask me i just feel more in control than with server components and now I have to do the is loading here. So is loading. If is loading. We're going to go ahead and we are going to return the page loader component. If there is no initial values, we are going to return back page error component and a message of project not found like this. and then we can pass in the initial values here to be initial values. There we go. And then let's just go ahead and separate these two. So let me go here. There we go. Let's go back inside of here and let's simply return project ID settings client from dot slash client. And we can remove the edit project form. There we go. Like this, which now means that inside of our hook to update the project, so features, projects, API, use update project. Inside of here, we no longer have to do the router thing, thankfully, because that just causes a very ugly refresh here. There we go. We are just invalidating the queries here, and that's way smoother. So now if I refresh, we have a nice loading here. There we go. And I can go ahead and change this to 444. Click Save Changes. And look how smoothly this looks now. Looks like we do have this issue here. That's interesting. So let's see why that is happening. That should definitely not happen. But I think it's because, yeah, if we refresh, it will be okay. So this is what, let's see, what should we do here? Okay, let's leave it like this for now. I think you know why it's happening. It's because we have to reload the initial values in the project, right? So maybe, oh, I know what we can do. So let's go inside of projects, components, edit project form. and on save, let's not reset the form at all. So nothing to do in the on success method here. So let me refresh again. And now I will change this to Antonio project and save. And there we go. Now, no need to do anything else. There we go. This is way smoother experience, if you ask me. Perfect. So we have that finished. What I'm interested in now is are we using any of the queries from here? Do I call get project anywhere? Nowhere. So I can safely delete the queries inside of my projects feature folder. Great. So that's one thing completely resolved here. So I'm going to reserve this chapter for refactoring that. Let's do that in this chapter. What I'm going to do next now is I want to refactor my members list here. I think that's also problematic here. And I feel like I also have the settings for the workspace, right? So let's do the settings for the workspace first. Yeah, it's also awfully slow on top of everything. So I'm going to go ahead inside of source, inside of my app folder, standalone, workspaces, workspace ID, and I'm going to go inside of settings page here. And I will no longer use the initial values here. So I will just remove them completely. I will also remove the props from here. No need for that. I will remove the interface. There we go. And I will copy this entire thing. And I will create a client.tsx inside of my workspace ID settings. I will mark this as use client. And I will export const workspace ID settings client. And I will just return this. I will import the edit workspace form here. and I will go back here and simply return the workspace ID settings client from dot slash client and I can then remove the edit workspace here. There we go. Now it will cause an error of course so we have to go inside of here and what we have to do is the same thing that we just did in the project settings. So what I will do is I will just copy this entire thing here and then I will just replace the values as needed. So I will add this. Instead of use project ID, I'm going to call use workspace ID. I'm going to go ahead and rename this to workspace ID. I will call this use get workspace. And it looks like we don't have use get workspace for the very same reason. So let's go ahead and see whether we have to develop that. So I'm just going to leave this like this. So leave it broken. Let's go inside of our features, workspaces. So in the API, yeah, we don't have use get individual workspace. So let's go into the server route.ts and let's see, do we even have it implemented here? So this is my post method, my patch method, my delete method, my post method, my another post method, but no get method here for the individual workspace ID. Let's go ahead and let's add it here. But regardless of if we are going to use server components or not, it's good to have all API routes as well. If you, for example, want to have a mobile application, right? You can't use the queries for server components for that. Let's go ahead and let's extract the user from c.getUser. Let's extract the databases from c.getDatabases. And let's extract the workspace ID from cRequestParam. And now let's get the member. using OA to get member, which we already have here. Great. And we can pass in the databases. We have the workspace ID and user ID will simply be my user.$ID. Great. If there is no member, I'm going to return c.json here with an error of unauthorized and a 401 status code. and then I'm going to go ahead and simply get the workspace to be await databases.getDocument. I will pass in the individual database ID and then the workspaces IDs and then finally my workspace ID here and I will return c.json data workspace. There we go. That is our protected get workspace ID route. We can now copy from our newly created projects API use get project and let's paste it inside of the workspaces API use get project. Let's rename it to use get workspace and let's rename this to use get workspace props. Let's change this to workspace ID. This will be use get workspace. Let's change this. Let's change this. Let's call this workspace and workspace ID. And let's change this to workspaces, workspace ID, and workspace ID. Failed to fetch workspace. And that's it. Great. We now have use get workspace. And now let's go inside of the client and let's import use get workspace from features, workspaces, API, use get workspace. Great. You can see how we even see the things we forgot to implement once we get rid of the server components here. So I'm glad we're doing this refactor. Let's go ahead and import the page loader. Let's import the page error here like this. And now we are going to have the initial values here, but it looks like what I don't do here is define the workspace type. So let's see, inside of my workspace is server. When I call this workspace, get document, I don't define the type of the document. Do I even have the type? I do have the type, excellent. So we are going to append that type here, workspace. And I already have it imported\nhere from .slash types. And now my error is gone. So I can now go ahead and refresh this. And there we go. So what I should be able to do now is the same thing here. Let me just go inside of edit workspace form. And let's go inside of the on submit method. Let's find the on submit. So we call form reset, but no need for that because the values will be reset And it will just look like it wasn't saved. So I will refresh this page now. Let's call this other workspace 123. Let's save it. And we got the ugly refresh because we forgot to update inside of our Workspaces API. Use update workspace here uses router refresh. We don't want that. We don't like that. So let's remove that here. There we go. Let's try it again. And let's hope for a smoother experience. So 4.3.1, save changes. There we go. That is what I wanted to experience, a nice, smooth experience when updating my workspace. And now I want to go ahead and do the same thing, but for the members list. And I think that that will be the last of my server components here. So let's go ahead inside of source. Let's go inside of app folder, standalone workspaces. Let's go inside of workspace ID members. We also have join. Okay, so we're going to have to refactor that as well. Let's do members for now. Oh, and this looks good. So we use get, oh, great. So we're not using anything inside of here. Perfect. No need to refactor members then. Let's go inside of join. What about here? In here we do. We're using get workspace info here. So this is what we are going to do now. Well, actually, I don't think we have to refactor the workspace ID join page because this page will not be visited after someone updates. So that router refresh will not really affect it. But still, I just refactor the rest, so I want to be consistent here. Let's do the following. Let's go inside of my features, inside of my workspaces, API, use get work, my apologies, server, route.ds. And we're going to implement a pretty similar route. So I will copy this route to get the workspace. I will chain it. And I will rename this workspace ID slash info like this. And for this one, it doesn't matter if we are a member or not, because we will simply be returning. an ID. Let's give that a workspace ID. Name. And I think also maybe an image. Workspace image URL. So let me go ahead and collapse this and this like that. So inside of my data object here. So let's see, is that all we need? We can confirm that. We are going inside of our workspaces queries. So inside of here, we have get workspace info. Oh, so we only return name. Great. Okay. So we definitely handle that. So inside of the workspace ID join page here, I'm going to remove the initial values entirely. I will remove the params. I will remove this. I will remove get workspace info. I don't need it. I don't want it. And then I'm going to go inside and create a client.tsx. Let's mark it as use client. Let's export const. What's the name of this workspace ID join client? Like this. And I'm simply going to return this entire thing inside. like this. Let's import the use join workspace form here like that. And now we're going to get the initial values here. So instead of this, we can import workspace ID join client here, and we can remove the form import from here. So again, our pages are only responsible for redirecting stuff. We are not going to handle them for initial values. It just doesn't look like a nice experience. So what I have to do now is go inside of my features, Workspaces API, I will wait to copy use get workspace, so individual one, and rename this use get workspace info like this. Let's go ahead and rename this to use get workspace info, use get workspace info, and use get workspace info props. So this is workspace info, workspace dash info here. So Workspaces ID, info, fail to fetch Workspace info. So we differentiate in our errors here. Great. Now we can go inside of our currently erroring client here. And I'm just going to give it the same treatment as I did with the rest of our pages. For example, the settings client here. So let's just copy all of these things here. Let's add it here. Use Workspace ID. instead of use get workspace it will be use get workspace info this is a page loader component and this is a page error component like this there we go so i think that this could be the last of our uh queries which we used for initial values here i think that now there we go this is a nicer experience now okay and what i'm interested in is where else do i use my queries so let's go inside of features let's go inside of workspaces and i have the queries here so let's see get workspace info should now no longer be used anywhere so i can remove get workspace info from here that's great and i will also remove the info props let's see uh get workspace, also not used anywhere. Perfect. So I will remove that entire thing. And now we have the get workspaces. And I think that this can stay. The reason I'm okay with this staying is because it genuinely helps us, right? So inside of our dashboard here, we use it to redirect to slash create or slash workspaces. So I'm okay with that because server components are perfect for redirection. It's done on the server side, so it's safe. And it's also more pleasant to debug rather than having the use effect, you know, magic happening. So I'm okay with that. I will leave the query to be for that. Let's see, do I have any more queries? Whoops, my apologies. I have accidentally closed in my Visual Studio code. So let's search again for queries.ts. Let's see, do I use queries? Okay, so we have get current here. Okay, so out queries, out queries, out queries, workspaces, queries, out queries, out queries, out queries. Perfect. So I think that's all the place where we are using the queries. So we are no longer using anything besides out queries, which is completely fine. So the only place where we are using workspaces queries is here to redirect. That's perfectly fine. So I think we successfully refactored all of that, which means that we can find last places where we use router.refresh and remove all of these things. I'm only okay with use login and use logout. So only leave them for the ALF APIs. But for deleting the project, no need. Remove the router, remove the router, remove router refresh. For deleting the task, no need. Remove the router refresh, remove this, and remove this, because we will not be using server components to load that data. For use update task, no need for router refresh, no need for this, no need for that. For use reset invite code, no need for this, no need for this, no need for any of that. Perfect. So this should now be working here. The only place where we are now using our workspaces is inside of, where we are now using the queries, the server component thing, is inside of dashboard workspaces page right here. There we go. So we redirect to the correct workspace. Perfect. So do we have any other queries? No. Great. Now let's go ahead and let's just check out our experience now. So for example, now if I go inside of my tasks here, let's say, let's just wait for a second for this to load. And now if I go ahead and create a new task, it should not cause any big refreshes on my screen here. It should just simply add the task. If I go and update it, it should again not cause any big refreshes. There we go. It just updates. It's so smooth now. If I delete the task, again, so smooth. Perfect. If I add a new project, it should just add there. It creates a new project and redirects me here. No huge refreshes, nothing clunky. Perfect. I absolutely love this. And now what I'm going to do is go inside of source app folder here, inside of dashboard, and I will remove the error and the loading from here. No need for it. So remove the error and the loading here. And let's see inside of standalone error loading, remove it here as well. out error loading, remove it here as well. So I should have only one error.tsx inside of my entire app and one loading.tsx inside of my app. Perfect. So we just refactor our app to not rely on server components for data fetching, except if we need redirects done. And if you want to, these loaders seem to be kind of high. So if you want to resolve that, you can go inside of page loader here and you can give them a height of screen like this. And then they will be a bit more aligned, I believe. Let's just wait. So if I go inside of settings here, yeah, there we go. Now they're not, you know, jumping up and down. They a bit more consistent here So let see if I go to the individual task details now Let see There we go This now looks just fine Perfect And now none of this causes any big refreshes, as you can see. And oh yeah, once we save this, what we should do is we should change this to stop being is editing state. Let's quickly fix that before we close the chapter. So go inside of task description component. And inside of here, after we save, go ahead and add on success here and simply call set is editing to be false. And then when you click save here, there we go. Great, great job. Let's go ahead and let's wrap up the individual project ID page by introducing analytics. and once we get analytics, we will be able to copy and paste that and wrap up the homepage. So what I want to do is I want to go inside of my features and I want to go inside of projects, API, my apologies, server, route.ts. And then what I'm going to do is simply at the end of all my routes, I'm going to get a get route right here and I will be loading slash project ID slash analytics. Let's add a session middleware here. And let's add an asynchronous controller. And as always, let's do our usual checks here. So we are going to destructure the databases and the user here. Then let's get the project ID from CRequestParams. And once we have that, let's go ahead and let's fetch the project. And I have to replace this with ProjectsID. And then let's go ahead and let's get our member. And if we don't have the member, let's throw back an error. So next thing we have to do is we have to import all the necessary functions and utils from date FNS in order to do those analytics. So let's import end of month, start of month, and sub months from date FNS. And let's go ahead and let's start by defining my now, right? So const now is new date. After that, let's append this month start, which will be start of month now. After that, this month end, which is end of month now. After that, last month start, so we can compare with previous data. Start of month, sub months, now and one. and this will be last month end, end of month, sub months, now and one. Like that. So let me just double check whether this is okay. There we go. So double check that this uses the correct naming and the correct logic here. Now let's go ahead and let's load all tasks from this month. So const this month tasks will be await databases dot list documents. Database ID tasks ID. And let's add a query equal. We are only looking inside of this project ID. Then let's add a query greater than equal. Dollar sign created at this month start. to ISO string. And query less than equal created at this month start to ISO string. So this is our query to get this month's task for this project inside of a range of this month. and less than equal here should be this month end. My apologies. Like this. Now we can copy this entire method here. Add it here and let's replace this with last month tasks. Like that. And instead of using this month start, we're going to be using last month start and this will be last month end. So now we have this month's tasks and last month tasks. And what we are now going to do is we're going to get the task count here. So this month tasks.total. And then let's get the task difference so we can display it on the front end. The task difference is task count minus last month tasks.total. Like that. Now let's go ahead and let's copy the this month tasks here. And let's rename this variable to this month assigned tasks. So we want to see which projects do we have assigned here. So we're going to add another query equal here, assignee ID to be member dot dollar sign ID. So now we have this month assigned tasks. Let's copy and paste it so we can create the last month assigned task. So let's do last month assigned tasks. And we are simply going to change this to be last month start and last month end. There we go. Now that we have both of those, we can go ahead and create the same total and difference here. So let's go ahead and create a constant assigned task count to be this month assigned tasks. So the first variable dot total. And then let's go ahead and let's add an assigned task difference to be the assigned task count minus last month assigned tasks dot total. There we go. So we now have that. And what we have to do now is we have to add incomplete tasks. So what I'm going to do is I'm going to copy both this month assigned tasks and last month assigned tasks. I'm going to copy them and add them here. So what we're going to do is rename it to this month incomplete tasks. And what we're going to do is remove the assignee ID and instead let's add query not equal status task status from features tasks types dot done. So we are only querying those which don't have done status. Make sure you have added an import for this. All right, so that is it for this month incomplete tasks inside of this project ID here. And then let's go ahead and let's copy this query here. For last month incomplete task. And replace the assignee ID with that query here. The rest can stay the same here. So task status is not done. That's great. And now let's go ahead and let's do the same thing we always do. So let's get the total and the count. So incomplete task count is this month's incomplete tasks dot total. And incomplete task difference is incomplete tasks count minus last month's incomplete tasks total. So we have both of these values right here. Great. And now let's go ahead and add the completed tasks in the same manner. So I'm just going to copy. this as well. So let's start from here and let's get these two. There we go. So we don't have to write it every time. There we go. And let me just indent all of them. So instead of this month incomplete tasks, these ones are going to be this month completed tasks, which have an equal status of task status done. And these are going to be last month completed tasks, which are going to have equal status to done. And then we can change this to completed task count to use this amount completed tasks. Make sure you change that this month completed tasks. And this here will be completed task difference. And make sure it uses the last month completed tasks here. And make sure it uses the completed task count here like this. So double check since we copied, make sure you don't have accidentally incomplete somewhere here. Otherwise, it's not going to work. And one more we have to do, and those are the overdue tasks. So again, I'm going to copy the difference, the count last month and this month here. Let me copy that and let me add it here after this. Okay, let's indented. So this will be this month overdue tasks. And how we're going to check that is if query not equal, so if they're not done, and also what we have to do is if query.less than due date is now to ISO string. So we have to add these two. If they are not done and if due date is past today. Great. So we have that and that is this month overdue tasks. Let's copy this two and let's add them here. And let's change this to last month overdue tasks. Now let's change this to be overdue task count. And let's make sure we use this month overdue tasks here. And let's change this to be overdue task difference. Make sure it uses overdue task count. And last month overdue tasks like this. And now we can finally go ahead and return c.json data. Task count and task difference. Assigned task count and assigned task difference. We can then add completed task count and completed task difference After that incomplete task count incomplete that task difference overdue task count, and overdue task difference. So make sure you have all of those. Let me just double check. There we go. All of these are necessary. Great. So we now have our analytics here to load and compare with the last month, all kinds of various things. So what we can do now is we can go ahead and create that API here. Let's go ahead and change useGetProject. Let's copy it. UseGetProjectAnalytics. UseGetProjectAnalytics props. UseGetProjectAnalytics. Change the props here. project-analytics. And we are using project ID and then analytics.get. And this will be failed to fetch project analytics. Everything else is the same. Great. So we now have use get project analytics. So we can now go inside of project ID. So we have inside of our dashboard, workspaces, workspace ID, projects, project ID, page.tsx. Let's go inside of client.tsx. And then in here, I'm going to add my data and isLoading for use getProjectAnalytics and pass in projectID. And I'm going to now remap those two. So this will be project and this will be isLoadingProject. This will be analytics and this will be isLoadingAnalytics. Let's go ahead and unify const is loading will be is loading project or is loading analytics like this. And inside of here, we're simply going to check for both. If there is no project available or if there is no analytics, we're going to return project not found. And now instead of using data, data, data, data, and data, so be careful to change all of this, we're using project. There we go. So I can refresh now and this should be able to work just fine. Let's take a look. There we go. So if you want to, you can be less strict with analytics in case analytics fall. For example, you can go ahead and not show the page error. Instead, we will just not show the component. For example, that's an option for you. Great. And I think I'm going to do it like that. Yeah, it makes more sense. Okay. If analytics fail, no need to not be able to show the user the project page. So what I want to do now is I want to create the analytics component. So we're going to do that. Just before we render the task view switcher, let's add the analytics and pass in the data as analytics like this. And now we have to create the analytics component. So we're going to do that in reusable components. Instead of source components, let's create analytics.tsx. Let's go ahead and let's create an interface, analytics props. And it's going to accept an optional data like this. So we can simply return null if there are no analytics to be loaded. And since we have to type strictly now everything that we just had inside of our, where is it, projects server. So basically we have to strictly type inside of the props all of this. So if you want to, you can copy them from here and then just paste them here and give them all the following. Task count will be number. Task difference will be number. Project count and project difference will be the optional things. Why? Because we're going to have analytics for the workspace ID as well. But right now we are doing analytics for individual projects. So we did not implement this. So no worries if you don't have this. Just add it and make it optional. Whoops. So project count and project difference is optional. Incomplete task count and incomplete task difference is also going to be optional. Then we have assigned task count, assigned task difference, completed task count, completed task difference, overdue task count, and overdue task difference. Great. Now let's export const analytics. Let's assign analytics props. And we can simply destructure data from here. and let's go ahead and let's do if there is no data, let's return null. And I just got an idea about how maybe we can improve this. If we go into the views, get analytics, use, get project analytics, I think that in here we could export type response type and make it infer response type from HONO. if you already wrote it, I'm sorry, but I think there is an easier solution type of client. Let's see. Client projects, project ID analytics 200. I think that this might give me, okay, it does not give me, oh, because I also have to specify the method get. So now response type here, there we go. We have the response type. So maybe we can use this instead. Let's call this project analytics response type and export it from your use get project analytics. And then inside of your analytics.tsx, you can use that instead. So let's do it like this. Just a second. I want to see exactly what type they are. So they have the data. Okay. We're going to go here and we're simply going to assign that project analytics response type from features, projects, use get API project analytics like this. And then we're going to have to modify this because we expect the data to always exist. So, okay. Let's do it like that. And data here now, you can see, is typed exactly as we return it. Okay, I think this is more type safe because you now cannot make mistakes. Great. So we have the analytics now. Now let's go ahead and let's add a return here. Let's add a scroll area from .slasht ui scroll area. This is from ShatCN. So if you don't have this, make sure you do Banex Shatzian 2.1.0 add scroll area. Let me just quickly show you again. Banex-Banex Shatzian 2.1.0 add scroll area like this. Or NPX Shatzian 2.1.0 add scroll area. What matters is the version. Great. So we have this. And now let's go ahead and give this scroll area a class name of border, rounded, large, pool width, and white space, no wrap, and shrink zero. And then let's go back instead of our client and let's import the analytics from components analytics. So it's reusable. Let me just move that here. There we go. And we have to pass in the data analytics. So what I'm going to do here is if there is no analytics, I'm just going to go ahead. Only if there is analytics, I'm going to render this. So if there's analytics, I'm going to go ahead and render analytics. Otherwise, I'm going to render null. Okay, that way we fix the typo, the lint error there. And now inside of here, we're going to add a div with a class name. Pull width, flex and flex row. and then I'm going to check if data project count. Okay, we don't have the project count here. So what we're going to do, well, let's just continue with what we have. Div with a class name, flexItemCenter and flex1. And then inside of here, we have to develop an analytics card. So let's go ahead and do that instead of components here, analyticsCard.tsx. And let's go ahead and let's create an interface. analytics card props. We'll accept a title, which is a string, a value, which is a number, a variant of up or down, meaning increase or decrease. So you can call it like that if you want to, and the increase value. And let's export const analytics card. Let's go ahead and let's destructure all the props that we need. And let's assign the analytics card props here. There we go. And for now, let's just return the analytics card. Let's go inside of the analytics and let's import analytics card from .slash analytics card. And let's go ahead and let's give it a title, total tasks. Let's give it a value, data task count. Let's give it a variant. If data task difference is larger than zero, it's going to be up. Otherwise, it's going to be down. Data task difference. Why is that not working? Oh, like this. And increase in value will be data task difference. There we go. So, we now have this and let's go inside of the analytics card and let's actually make this look cool. So let's go ahead and import everything we need here. So from react icons slash FA import FA carat down and FA carat up. From atlib utils import CN and from components UI card, import card, card header, card description and card title. And now let's go ahead and let's actually develop this. So first of all, I'm going to add some constants here. Icon color. If variant is up, we're going to do text emerald 500, otherwise text red 500. Then I'm going to do increase value color. If variant is up, in that case it going to be text emerald 500 otherwise text Red 500 So pretty much the same as above Perhaps we can even reuse this constant And let's do a const icon with a capital I, so we can use it as JSX. If variant is up, in that case, we're going to use FA caret up, otherwise FA caret down. Great. So we have this for the icon. Now let's replace both of this with card, class name, shadow none, border none, and full width. Inside of here, I'm going to add a card header component and a div with a class name, flex item center, gap X 2.5. Inside, let's add a card description component. Let's give it a class name of flex items center, gap X2, font medium, and overflow hidden. Inside of the card description, let's go ahead and let's add a span element with title inside, and let's give it a class name of truncate text base. Like this. outside of the card description let's add a div with a class name plexitemscenter and gap x of 1 and let's go ahead and let's render our icon component with a class name cn icon color and size 4 below that let's enter a span element which will render the increase value and let's give it a class name of cn CN, increase value color, and truncate text base and font medium. And then let's go ahead and let's go here and add a card title and simply render the value inside. And let's give it a class name of 3XL and font semi-bold. and there we go you can see that now we have total tasks increase of one in uh in comparison to last month and the total tasks standing here right so this is only in this project if i go to project one let's see i don't think i have any tasks here project zero and zero is considered negative uh i'm going to leave it at that great now let's go back to the project id page specifically in the analytics here. And let's create the analytics for everything else we need. So we're going to copy these two and let's paste them. So I didn't include this instead of the analytics card. So it can be used standalone somewhere if you want to. But we need this wrapper around each of our analytics card for this analytics component here. So it's scrollable on mobile. So my next analytics card will include assigned tasks and it will use the assigned task count. It will use the assigned task difference and data assigned task difference. So now I have my assigned tasks here. Great. Let's go ahead and let's copy and paste this. So the next one here will be my completed tasks. So completed tasks. I'm going to use my completed task count right here and completed task difference right here and completed task difference here like this. And what I'm forgetting to add in each of this is a separator. And we can leverage our dotted separator because we've made it so that it can be vertical as well. So let me just import from my components here dotted separator from dot slash dotted separator. So I'm just going to go ahead and move it here. And I'm going to add it at the end of each analytics card. So dotted separator with a direction vertical like this. And here. And here. There we go. So you can see how nice our analytics are now separated. Great. Let's go ahead and let's copy and paste this one. And besides the completed tasks, we are also going to have overdue tasks. So overdue tasks. Let's go ahead and let's give this the overdue task count. the overdue task difference and overdue task difference here as well. There we go. So total tasks, assigned tasks, completed tasks, and overdue tasks. We also have the incomplete tasks. So let's go ahead and add that as well. I'm going to add that as the last one here. So this will be Incomplete tasks, so data incomplete task count, incomplete task difference, and incomplete task difference here. There we go. Just like that. And you can see how on mobile, this is supposed to scroll, but it's not scrolling. Okay, let's fix that. So my scroll area should definitely make this scrollable. So what I'm going to do is I'm just going to pause and play around and see why it's not working. What is wrong is that we are missing one more component from scroll area, and that is scroll bar. And we have to add it right here. Scroll bar like this. And let's give it an orientation. Or vertical. My apologies. Horizontal. And now this should be scrollable. There we go. So we can now add as many of these projects as you want. And when they become too large here, you can see how they have a nice scrollable effect here. And this is especially visible on mobile. If you think this is too much tasks, you can go ahead and remove one analytics card if you don't need it. Great. So we've wrapped up our project ID page. So what we're going to do next is we're going to do the same on the homepage here. So we're going to load some information about the current workspace ID. And we're going to extend this analytics because inside of our workspace, we can have some analytics which projects don't have. So we're going to have to make this be modular and we're going to have to combine two different response types. So we're going to see how we're going to play around with that. Or maybe to keep it simple, we can just use the same analytics from both of these. Nevertheless, great, great job. Let's go ahead and let's implement the workspace analytics. We can do that quite easily by first going inside of features, projects, and simply copying our recently created project ID analytics. So let's go ahead and let's copy this entire thing all the way to the end. and let's go inside of our workspaces server route.ds and I'm going to go ahead and simply go all the way to the end of my router here and I will chain that route. We're now going to fix all of these errors. So let's start by renaming this to workspace ID and then fixing the destructuring of the param. We don't need to load the project because we directly have workspace ID to get the member here. Then let's go ahead and let's import everything we need from our date FNS here. So let me simply add that here. Then let's import tasks ID from config. And let's also import task status from features, task types. So this is what I added, features, task types, and I have added tasks ID from config here. And then go ahead and find every query which does query.equal project ID, project ID like this. And you're going to have to modify it, right? So you can see how many of them there are here. And what you can do is you can do find and you can replace it with query equal workspace ID and use the workspace ID here like this. So just make sure that you didn't accidentally search for the one with the comma because you're then going to have to add commas everywhere and click replace all. And there we go. So you can see how now all of my queries here are looking for workspace ID instead of project ID. So I no longer use project ID anywhere. I use workspace ID in place of that. And that's it. That's our route for analytics inside of workspaces. Now, let me close all of this and let's go inside of projects API, use get project analytics. Let's go inside of workspaces and paste that. Let's go ahead and let's rename this to use get workspace analytics. Let's rename this to use get workspace analytics. Change the prop to be workspace ID. Change the infer response type to go to workspaces and use the individual workspace ID here analytics. Everything else can stay the same. Use the workspace ID. Change this to workspace analytics for this workspace ID, change this to be Workspaces, Workspace ID, and Workspace ID param, and finally fail to fetch Workspace Analytics. There we go. That's our UseGet Workspace Analytics hook. Now, let's go inside of Source, App Folder, Dashboard, Workspaces, Workspace ID page, and inside of here, let's return workspace id client, which doesn't exist yet. So we're going to create it in a second. So inside of this, let's create a client.tsx. Let's mark it as use client. Let's export const workspace id client. And let's return a div here. Let's go back inside of page.tsx and let's import this from dot slash client. Now we will continue developing inside of the workspace id client. I'm going to start by adding WorkspaceID from UseWorkspaceID here, and then I'm going to add UseGetWorkspaceAnalytics and pass in my WorkspaceID here. From here, I can destructure the data and remap it to Analytics, and I can destructure IsLoading and remap that to IsLoadingAnalytics. I'm going to copy this a few times.\nSecond one, I'm going to go use get tasks. So make sure you've added an import for that. Then I'm going to do use get projects. So make sure you have added this, use get projects, and use get members. So make sure you have added all of those. I like to order them by length like that. I'm going to remap the ones for use get tasks to be tasks and is loading tasks. Then we have projects and is loading projects. And finally, we have members and is loading members. Now let's go ahead and let's do the structure. Use create project model. And let's get use create task model. So make sure you have added those. So this is my project model and this is my task model imports. Make sure you have both of them here. We're going to do the structure open from both of them. And since they are named the same, let's remap them to create project. And let's get this one open and let's remap it to create task. Then let's go ahead and let's create a unified isLoading like this to use isLoadingAnalytics, isLoadingTasks, isLoadingProjects, or isLoadingMembers. So if any of that is true, we're going to go ahead and do the following. If isLoading, we will return the page loader component. and you can get it from components page loader. Let me just separate it from the rest. There we go. Otherwise, if there are no analytics, no tasks, or no projects, or no members, you can go ahead and return a page error here with a message, fail to load workspace data. and make sure you have imported the page error from components page error like this. And then finally, we can start developing this page. So give it a full height, flex, flex columns, space Y4. Let's go ahead and add analytics component. We can get that from components analytics here. And let's simply pass in the data to be analytics. and now if you take a look you can see that I have two tasks here but inside of my individual projects I have only one task for this project right and here we have a little bug these tasks load all projects so that's a bit of a mistake but you can see how I have one task per project so the analytics in individual projects say you only have one task here but when you go to home page, you have two tasks because we are analyzing on a workspace level. Great. So after the analytics, let's go ahead and let's define a grid here with a class name, grid, grid columns, one, Excel grid columns, two, and gap four. Now I'm going to go ahead and do the following. So I'm going to create new components inside of here. Let's go ahead and let's export const task list here and let's go ahead and let's accept tasks here. My apologies, this is what I wanted to do. So we will accept tasks, which will be task list props. So interface task list props. Let's go ahead and let's create the tasks here to be a type of task and an array like this. So task list props. We have imported task here from features task types like this. Let's go ahead and return this. And inside of here, we're going to go ahead and return a div with a last name, flex flex column gap y4 and call span 1. And we're going to say tasks. Now let's copy the task list and let's render it inside of here. There we go. So now you should see the tasks here. And let's pass in the tasks. Or we can just make this be data, like this data here. And then inside of here, we pass data, which will be tasks.documents, like this. Actually, let's pass the, we also need the total here. So, let's pass tasks.total, like this. So, total will be a number. There we go. So, I want to separate this into components, so it's easier to develop. Great. So, we have data, which we can iterate over, and we have total here. Perfect. So let's go ahead and do the following. I'm going to go ahead and create a div here for this task list and give it a class name of background muted, rounded large, and padding of four. Then inside of here, I'm going to create a new div, which will encapsulate my title and my new task button. So let's give it a flex item center and justify between. And now let's go ahead inside of here and let's add a paragraph, tasks, and then inside let's do total. Let's give this div a paragraph, a class name of text large and font semi bold. And then let's go ahead and let's add a button from components UI button with a variant of muted. And let's go ahead and give it a size of icon. And let's give it an on click. And in here, what we need to do, well, we can do that here. Yeah, we can just move this create task model here inside of the task list component because we're not going to need it anywhere else. So yeah, you can do that here. And then simply add the create task here. There we go. So you can then remove it from the main component, the workspace ID client. Great. And inside of here, we're going to add a pens a plus icon from Lucid React. And let's give it a class name of size four and text neutral 400. There we go. So now this task should open a new, create a new task. Great. So it works fine. Now we have to iterate over our tasks and render them. So after this, they've encapsulating the title and new button. Let's add a dotted separator component. and let's give it a class name of my4. So just, you know, be careful. Make sure you're importing these things, right? I have added like Lucida React here. I have added components dotted separator. And okay, while we are here, how about we add all imports which we are going to need? So from Lucida React, we're going to need the calendar icon and settings icon alongside the plus icon. From date FNS, we're going to need the format distance to now and we're going to need to import the link from next slash link. And alongside these buttons here, we're also going to need the card and card content from components UI card. And that's all the imports we're going to need. So now let's go back to our dotted separator here. Let's create an unordered list here with a class name. flex flex column and gap y of 4 like this. And then we can iterate over the data here. So we will do data.map, get the individual task. And inside of here, let's add a list element with a key task ID. And then let's add a link to be href slash workspaces. Then we have to add a workspace ID, which right now doesn't exist. So we're going to add it in a second, slash tasks, and then slash task dot dollar sign ID. So we need to get the workspace ID inside of the task list component here, and we can use this from here. We already have the import, so why not use it? There we go, workspace ID. And inside of here, let's go ahead and let's add a card component. Let's give it a class name of shadow none, rounded, large, hover, opacity, 75, and transition. Like this, let's add card content here. Let's give it a class name of padding 4. Then let's add a paragraph, task.name here. Let's give it a class name of text large, font medium, and truncate. After the task name, let's add a div for the class name, flexItemCenter and gapX of 2. Inside of here, let's add a paragraph which will render task.project.name. And let's go ahead and put a question mark here in case we are unable to load the project like this. and then let's add a div here with a class name. This will be actually a self-closing div. So let's do that like this. It will have a size of one rounded pool and BG neutral 300. So it's like a little separator. And then we're going to have a div, which is going to render two items. So let's give it a text small, text muted foreground, plex and items center let's render a calendar icon which we have an import for let's give it a class name of size 3 and mr of 1 let's add a span element here whoops so span element which will use format distance to now new date task due date which is a string and let's give a span here, a class name of size 3 and MR of 1. My apologies. Truncate. That's what we need to add here. Great. Now, outside of this iteration, but still inside of the unordered list component, let's add the special list item, which will be no tasks found. And let's go ahead and give this a class name of text small, text muted foreground, text center, like this. So right now, this is visible. And we're now going to hide it and only show it if this list item is the only list item inside of the unordered list here. And we can do that quite easily by simply doing first of type block So it only going to be visible if it the first in the array So if I change this to an empty array you can see it says no tasks found But if there is data here, then data will be shown here. Great. So now let's go outside of the unordered list and let's add a button and let's say show all. Let's give it a variant of muted. Let's give it a class name of margin top four and full width. And then let's add a link. Let's make an Ahrefs go to, whoa, slash workspaces, workspace ID, and then all tasks. And let's give this an as child prop. So now this button will simply redirect the user to the tasks page. Great. okay and that's it for our task list component so what i want to do now is well let's separate this component how about that i'm going to do that here actually i think it's fine to keep it inside of one page i don't think it will cause any harm but let's copy it and let's paste it here and let's rename this to project list and this will be project list props and let's change this to project list props. The data here will be a project, right? From, I have to import project type from feature projects types. So let's add it here. There we go. And we are going to use the settings icon later. Great, so we have the project list props and now let's go ahead into our main workspace ID client here and below the task list, Let's add the project list with data, projects, documents, and total, projects, total. There we go. All right. So we have a mistake here because you can see that our projects here use get projects, which are apiprojects.get. So let's go inside of features projects server route. And let's find our get here. And probably when we do list documents, we didn't define the type. So now let's add the project type inside of list documents here. And now you can see that we are no longer going to have the issue here with passing those documents. Previously, we had an error because the documents didn't match our data, which we have defined here in the project list. Great. So now, instead of here, we have a project. So what we are going to do inside of here is the following. We're going to change this to be projects. We're going to change this to use create project. We don't have this yet, and we have a typo, so create project. And we can simply use this one from our main workspace ID client. Let's remove the use create project model, and let's add it here inside of our new component instead of the use create task model. There we go. So now it's used here. So projects total. Then we will be working with the individual project here. So project ID, this will be projects and then project.id. Then inside of here, we will change this to have a project name. And then we can, well, how about we clear everything inside of the card content for now and refresh. Now we should get rid of any errors here. let's there we go so now we have projects here great so what i want to do now is uh modify this to show the projects so uh let's go ahead and modify the card content alongside having padding four let's give it the flex items center and gap x 2.5 like this uh inside of the card content we're going to have project avatar so make sure you have imported project avatar from features components project avatar. Let's go down here and let's give this component a name, project.name, image, project image URL. Let's give it a class name of size 12. Let's give it a fallback class name of text large, like this. And besides that, let's give it a paragraph, project.name, with a class name, text large, font medium, and truncate. There we go. So now we have a list of our projects here. So the only thing that I would like to change is the following. So my unordered list will no longer be flex. Instead, it's going to be grid, grid columns one, and on LG, grid columns two and gap four. So now on large devices, this is how it's going to look like. Great. And let's go ahead and change the following. I want to change this one to be, so instead of project list, let's change this div from background muted to background white. And let's give it a border like this. There we go. So a little bit of differentiation here. And let's go ahead and change this plus icon button to have a variant of secondary. like this. There we go. And no need for the show all button here. So let's remove the show all button. There we go. So now the button here should be removed. Let's just wait a second for this to load. And one thing left to develop here, which is the members list. So let's go ahead and let's copy this entire thing and let's add it here. So this will be now members list. The data will be individual member, which I'm pretty sure we don't have. So how about we go and develop it? So it's going to be quite simple. Inside of features, let's go inside of members, instead of types. And let's export type member. And let's give it a workspace ID, which is a string, a user ID, which is a string, and a role, which is a member role, like this. And then we can go inside of here and we can assign the member from where here. So features, members, types. like this. And now we also have to go inside of our features members server route.ts and find the get route. And here, when we list the documents, let's go ahead and do members. From dot dot slash types. But do we have any problems? Some member here, type member. Oh, yeah, we forgot to do. How do I do it in projects, for example? The type here extends models.document. So that's what I forgot to do. So it will be models.document and our extension like this. There we go. So now the route for my members should work just fine. There we go. So make sure you've added it into your get route here onto the list documents. You have given it a type, which it will return. Great. So now, instead of the client here, we have defined the members list to accept data, which is a type of member. Great. We don't need use create project model like that. We will need workspace ID. Let's call this members. And this button right here will have the settings icon. And this link will have an Ahrefs to go to slash workspaces, workspace ID slash members like this. And we're going to give this button an as child proc here. There we go. All right. And let's go ahead and modify this slightly here. So we're going to modify the UL. It's going to be a grid again. grid calls one on large grid on sm it's going to be grid calls two and on large it will be grid calls three like this then inside of here it's not going to have any link so we can remove the link here the card content here will be a little bit different so we are going to remove from the card itself hover and transition and let's give it overflow hidden and for the card content let's put the padding three flex flex column item center and gap x of two instead of rendering the project avatar we'll be rendering the member avatar component uh and let's just give it the class name of size 12 here and the name of well what will be member right so member.id member.name and then we're gonna have uh two uh we can add a div here around this paragraph because we're going to render the email as well. So this div here will be flex, flex column, items center, and overflow hidden. The first paragraph here will have a text of large font medium and align clamp one and render a member name. And then below it, we're going to have a text small. And instead of font medium, we're going to remove that and add text muted foreground and give this a member email like this. And this will say no members found like that. And let's also fix inside of our project list here to also use no projects found like this. And now let's go ahead and let's finally render this as well. So we're going to add our members list here. data will be members documents and total will be members total. There we go. So now, there we go. We have our members list right here. And when I click here, it will redirect me to my members list. Perfect. So when you click here, you can create a new project. When you click here, you can create a new task. Amazing. So what's left to do now is to go through the project and, you know, fix some bugs, perhaps fix some types, which we have. We're going to run a few commands to see about that and anything else we found that is missing or buggy. Nevertheless, you pretty much finished the entire project at this point. So props to that. And as always, amazing job. Now let go ahead and let fix some leftover mistakes So first thing I can remember is that when I select a specific project ID page here I'm still loading all tasks regardless of the project. So let's see how we can resolve that. I'm going to go inside of source, inside of my app folder, dashboard, workspaces, projects, project ID, client. And inside of here, I render the task view switcher. So let's see. I'm using the project ID from the task filters, but since there is no filter assigned in the URL, I don't send that. So how about we try and do this? How about alongside my workspace ID, I also get the project ID here. And let's call this default project ID. Use or maybe param project ID. So use project ID here. and import this from features projects hooks use project id there we go and i'm going to try to do this so the project id will be either param project id or my project id uh let's add a comma here which comes from the filters here so let's try this now See, there we go. So now when I click on test, for example, it should only load those in test, which are none. If I click on project one, it should only load the tasks, which are inside of project one. If I click on Antonio, only those in Antonio. If I click on my tasks, it should load both of them, except if I add, for example, test or Antonio project or project one. And if I select all, everything is good. There we go. So we use the inside of the task switcher component to resolve that issue. Another issue that I can see here is that my navigation has a static text here. So how about I change this depending on what I go to? So let's go ahead and close everything. And let's go inside of source components. And let's go inside of my navbar component here. So in here, I have this static text right here. So how about I go ahead and I create a const path named map, which will have tasks. And in that case, the title here will be my tasks. And the description here can be, for example, view all of your tasks here like this. And then I can have project and this will be title. Actually, this will be projects. So title here will be my project. description here will be view tasks of your project here. And now let's go ahead and let's mark this entire thing as use client. Let's import use path name from next navigation. And let's define default map here simply to be title of home and the description of what it already is here. Monitor all of your tasks and projects here. So description will be this. And then inside of here, we're going to get the path name from use path name. And then let's get the path name parts by using path name split by forward slash, because our URL is currently slash workspace slash workspace ID, and then, for example, tasks, or maybe it's projects slash project ID, right? So that's why we're going to have to focus on a specific path name key. So path name key will be path name parts third in the array as key of type of path name map. And then finally, we can do path name map using the path name key, or if we cannot find it, we will simply default to default map. And inside of here, we can now destructure the title and description. So we can simply replace this with the title, and we can replace this with description. There we go. So I think that now already, if I'm on project, it says my project. If I click on my tasks, it says my tasks. And if I click on home, it says home. Great. So that's two bugs resolved. Now what I want to see is in my terminal, I will shut down my app. What happens if we do one run build? So I want to see whether I have any outstanding errors that I will have to fix before my production build. This will most likely be some missing types and maybe some lint errors. There we go. So this will now tell us everything that we need to fix. Okay, let's go ahead and let's go inside of apiroute.ts. So that is located here, source API, apiroute.ts. So routes is assigned a value, but only used as a type. Okay, well, that's interesting. Const routes. So how exactly do I fix that? This is the new rule set from Next.js, but I honestly don't really understand how they want me to fix it. Okay, I'm just going to disable this. Slin disable next line type script no unused vars for this line because this is completely fine if you ask me. I'm not sure what kind of lint rule is that. Then we have features out server route dot DS. So let's go ahead and go inside of features out server route dot DS. We have import Zod. We are not using it. Great. Then we have edit project form. So let's search for edit project form. Inside of here, looks like we have is deleting project, which is assigned a value, but it's never used. So let's see, where do we use delete project? Where do we use handle delete? Here on the delete project. So I'm just going to add is deleting project here. So we are using it somewhere. And we also have a toast from Sonar, which we are never using. So let's remove that. And let's remove the copy icon from Lucid React if we are not using it as well. Okay. That's it for edit project form. Then we have the create task form. So create task form. In here, we have an error. I have assigned the router, but I have not using it. So let me remove that. And let me remove the import for the router, I assume. There we go. That resolves that. Then we have the data table component. So data table component. I don't know if you will have this error, but I have removed my search, so I don't need this input. So if you have no errors here, that's completely fine. No need to do anything then. That's it. And then edit task form. So edit task form here is telling me that I have a workspace ID and a router that I'm not using. So I have removed that. And now I'm removing the imports for that as well. There we go. and then I have event card here so let's go inside of event card and let's go ahead okay so assignee is unexpected any well I think that now we can actually use the member from features members types let me just go ahead and move it here let me move react from react here while I'm already doing this and where do I use the event card let's see so my event card I believe is used in my data calendar component. So now that I have modified this, I say need to be the member from features members types, which we have created recently. Let me see if my data calendar is working properly or is it having any issues here? So as I need here, it looks like it's working just fine. No errors in the data calendar here, at least what I can see. Great. So now let's go inside of tasks, types.ts. So tasks, types.ts, projects. We don't need it. Let's remove it. then we have appwrite.ts storage is defined but never used so appwrite.ts we have storage looks like we don't need to use it here uh really we don't we never use the storage I guess we uh session middleware session middleware uses storage okay I just wanted to confirm okay so I resolved everything that says here let's go ahead let's do run build again. And let's see if I still have some outstanding errors to fix or will this successfully build. Okay. I still have some errors here. So task server route.ts. Let's try that. So tasks server inside of my source features, taskserver route.ds inside of my bulk update. Is it the bulk update route? Yes. Workspace ID here is string or undefined. Okay. So I'm going to do if there is no workspace ID, I will return c.json with an error here. Workspace ID is required. and 400. Okay, let's do bun run build again. Basically, I'm just doing this until I can see my build passing because if this doesn't pass, we won't be able to deploy anywhere. And there we go. Now, there are no errors. So, what I'm going to do is just bun. No, sorry. I'm going to do rmrf.next like this because once you build your project, it can cause errors if you do bun run dev without deleting the next file first. Great. So let me just confirm that everything is still working just fine inside of my project here. Great. So we just fixed outstanding bugs. Great, great job. Now let's go ahead and let's implement OAuth logins. So let's go ahead and let's go inside of our AppRite console here. Go inside of Auth and go inside of settings. And I have already played around with this. So go ahead and find the GitHub OAuth provider. It should be down here somewhere if it's not enabled. And yours will probably be disabled and it will be completely empty like this besides the URI which is, as you can see, filled here. So now what you have to do is, first of all, click enable. Go inside of your GitHub here and you have to go inside of your settings. Go inside of developer settings and go inside of OAuth apps and inside create a new OAuth app I going to call this app right test My homepage URL here will be localhost 3000 and what matters is the authorization callback URL which you get from here This is your authorization callback URL. So copy that and paste it here and click register application. And then you're going to have your client ID here. So go ahead and add that to app ID. and click on generate a new secret to get the secret and copy it from here. And let's go ahead and add it to the app secret and click update. So now I'm going to go ahead and follow the documentation here. So I went into Auth, into SSR login, tutorials, Next.js, and then number seven, OAuth authentication with SSR. So let's start with creating this server action. So if you want to, for a challenge, you can refactor this to Hono. I'm now just going to follow this exactly as they have made it. You can definitely do it with Hono as well. So we're now going to go ahead inside and create the OAuth.js here. So go inside of lib and let's create OAuth, in our case, .ts. And let's create this server action. So we have to change this. It's not from lib server. Our app right is directly in lib. So it's right here. There we go. our create session client and our create admin clients are right here. Everything else here can stay the same like this. And now let's go ahead and let's skip the OAuth form. We're going to come to that in a second. Let's go inside of and create an OAuth callback. So we're going to do it exactly where they tell us to do it. And basically this is where your route will be. You can change this if you want it to be somewhere else. So I'm going to go ahead and do this. I will go inside of my app folder, and I will simply create a new folder OAuth. Because remember, oh yeah, mine already exists because I just tried this a second ago. So create a new folder called OAuth, and it's simply inside Route TS, and paste everything inside. So change or fix this import here. Make the request be a type of next request. which you can import from next server. And then you have this error here. So let's just do if there is no user ID, let's go ahead and let's return next response error like this. And the same thing if there is no secret. So like this. Or I think the proper way is to throw new next response and then simply say missing fields and then add a status of 400. Can we do that? There we go. Great. So we can do that here. And now this will redirect you back to, you know, wherever you want. So I'm going to choose, you know, just my forward slash, which is basically my workspaces, right? And I will change this to my auth cookie from features auth constants. like this. So let me just change this imports here. There we go. Let me remove the comment, which is falsy. I also have a comment here that's also falsy. This use server is required because this is a server action. Okay. We have this, we have this. Everything here seems just fine. I'm just unsure about this. So slash or out here. Okay. That's good. But I'm not sure what the second one does. Let's see if they have an explanation here. The OAuth provider redirects the user back to slash OAuth route. I'm just not sure what is the slash sign up. I don't have that route. So mine is slash sign dash up. So I think I have to modify it to that because inside of my OAuth, I have sign up. So perhaps that's the route. And I have OAuth, which is my, well, now it became a route here. So now we have to use this sign up with GitHub inside of our cards. So let's go inside of sign in card right here. And let's go ahead and let's import this. So I'm just going to add it here, import sign up with GitHub from at slash lib oauth. And what you have to do is simply find your login with GitHub and just simply pass it here. That's it. Just ensure that it has the user server here. And I think that this should already be working. So I will now refresh this. And I will click login with GitHub. And it looks like I have an error. Okay, a few classes or prototypes are not supported. Okay, so let's see what did I mess up here. Okay, I think we have to first wrap it like this. Let's try it like that. There we go. Now it works. And then let's click authorize. And this should redirect me. There we go. back to my page right here. Perfect. And I can now create a new workspace. I believe there is one issue we have though. And that is that this user that was just registered here inside of my, let's go inside of users here. You can see it doesn't have a name. That's the problem. It only has an email. So the all out from AppWrite doesn't assign them any names. So what we're going to have to do, and let's see why exactly is this not working. Okay, so user with the requested ID could not be found. Yeah, so there are some issues here, but let's see again what's going on. User with the requested ID could not be found. Okay, I think the reason this is happening to me is because I previously created this very same user and I have a member with their email. So I think for you, this might work just fine. But for me, it seems to having an error. So I'm going to go inside of members and I will simply delete all members here. I'm going to go inside of tasks. I will remove all tasks here. I'm going to go inside of projects. I will remove all projects here. And I'm going to go inside of workspaces and I will remove all workspaces here. So let's go here and let me go back to my localhost 3000. And now there we go. Test workspace. Let's see if this will work now. There we go. Now it works. But as you can see, we have a problem because if I were to create a new project, so test project. and now if I were to create a new task here, you can see that in my assignee, I cannot see the name of this user. So, what we can technically do here, let's see, instead of OAuth, there's nothing much I think we can do here, really, in create OAuth to token scopes. Yeah, it doesn't really give us any options, you know, to assign a username here. How about inside of app or out route, create session, it does not allow me to pass the name. If it allowed me to pass the name, that would be cool. So what we would do here is I suggest you search for user.name and find all the places where you do that. So for example, instead of source features, members, server, we do user.name. How about we do user.name or user.email? So we will use email as the name if it doesn't exist. And let's do the same thing instead of source features tasks server. So I'm going to go ahead and do user.email because email is something we are always going to have. And one more place here in the S&E. So user.email. and let's see, does that maybe improve developer user experience here? There we go. Yeah. So now we can work with emails. So it looks like that's how you can implement that. Perfect. And I will create this. Let's create task and let's see. There we go. Perfect. So this seems to work just fine. Amazing. And on my homepage here, I should now be able to see my name. There we go. Yeah, Not exactly perfect, but this is a challenge for you, how you would handle these all out users, let's say. Great. And instead of members, we have them here as well. Perfect. Nothing is crashing. That's important. We didn't do anything that crashes the app. Great. And now what we are going to do next is, well, deploy this. If you want to, you can follow the exact same instructions to add login with Google. So you just have to obtain the client ID and the server. And you have to go inside of your app, right? Inside of your, let's go here, ALF, inside of settings, and simply go ahead and find Google and enable it. So let's find it here. You have to add the app ID and the app secret. So since we already did GitHub, let me show you how to do Google in case you haven't done it yet. So type in Google Cloud Console and click on the first link. So console.cloud.google.com. Let's go ahead and let's create a new project here. I'm going to call this Jira clone and I will click create. Let's wait till the Jira clone project here is created. So let's just wait a second. Click Select Project. Confirm that you're inside of Jira Clone and search for APIs. Click on APIs and Services here and then go inside of OAuth Consent screen. Let's close this. Click External, Create, close this. Give your app a name, Jira Clone. select an email you can skip all of this here and now the authorized domains here let's go ahead and see can we add localhost 3000 or not can i add http here must not specify scheme okay looks like i cannot add this okay but let's see should i maybe add Add this. Okay. Create OAuth2ClientID with web application as the application type and then add this.\nDon't have to add this anywhere but the redirect URI. So I'm going to go back inside of my cloud console here. And I will simply add my tutorial mailing gmail.com email here. Let's click save and continue. Hopefully this will be enough for the OAuth consent screen. Click save and continue and save and continue. There we go. And then go inside of credentials. Create credentials OAuth client ID. Select the web application for the type. and go ahead and paste the authorized redirect URLs, which we just got from here. So copy this and put it here. And this one will be HTTP localhost 3000. Oops. Yeah, make sure you have localhost 3000 set here. So let's see if this will be enough now. There we go. I have the client ID. So let me go ahead and add it here. And I should have the client secret now here. So let's add that here and click update. There we go. Now I should have my Google authentication. I just didn't enable it. So click enable and update again. There we go. We now have Google enabled. And now let's go ahead and let's go inside of OAuth and let's copy this thing. sign up with Google and let's go ahead and change this to use OAuth provider Google. And hopefully the instructions will be exactly the same. Now let's go back inside the sign in card here. And I will just copy this and I will just add it here. Sign up with Google. So make sure you have added an import for this. And let's see if this will work now. So I will log out. I will go inside of my application. I will remove this user because I have only one email here. Both for GitHub and Google are the same. I will go inside of my databases. I will go inside of my tasks. I will remove my tasks. I will go inside of my projects. I will remove my projects. I will go inside of members and I will remove my members. and I will go inside the workspaces and I will remove my workspaces. So let's try this out now. I'm going to refresh this and I'm trying this on the sign in card. So let's see, log in with Google. Looks like this is working just fine. Let's confirm and let's cross our fingers. And there we go. We are logged in and looks like I actually have my name here. this is actually the name I put in. So it actually does read the name from some OAuth providers. That's interesting. Great. So that's how you can implement OAuth login. Just go ahead and also add this to your sign up card here. So let me add this to imports and let me go inside of my sign in card. Sign up with GitHub. So let me use that here. so let me find the button okay copy this and add it here as well except this will be google there we go so now hopefully if i log out from here and if i try and click from either if i click from sign up or sign in it should be exactly the same i don't think there should be any difference yeah so it will either create an account or simply allow me to log in. There we go. So we now have all out here as well. And since we just added a new feature, how about we go and shut down our app and simply try it, run, run, build again, just to see if this new scripts cause any errors. So we can immediately fix them before we deploy our application. There we go. Looks like no errors here. Linting went successful. Great. Amazing, amazing job. You just added all the features we said we are going to create. And now all that's left is to deploy. In order to deploy, we first have to push this project onto GitHub. So I'm going to call this next14jira clone. I will make this repository private and I will click create repository. Since this is an existing repository, I'm going to copy these three lines right here. Ensure that you have done git add and git commit for example deployment like this So I have nothing to commit So I getting an error here If you have nothing to commit you can go ahead and simply run copy these three lines here So let's push that. Now when I refresh here, I should have my project running here. Great. So now let's go to Vercel and let's create a new project. and there we go. Here is my project. I will click import here. Let's go ahead inside of environment variables here and let's assign everything we have in our dot environment dot local here. So I will just copy everything here and I'm going to go ahead and paste everything inside like this. And now what I'm going to do is I'm going to click deploy. Let's hope that's the only thing we have to do. So since we tested build locally, it should work just fine. But you know, you never know. So I'm going to pause the screen and unpause when it's finished. And there we go. Looks like my project was successfully deployed. So I'm going to go ahead and click continue to dashboard here. And let's go ahead and look at my Next14 Jira clone. And here's an important thing to know. This is your domain, the shorter URL, not the longer URL. So always use this URL when trying things. So I am now back. I'm now on that URL right here. So next we're in Jira clone. Let me go ahead and enable developer mode for you so you can see. There we go. And while I'm sure that some things will work here, there are some things we definitely have to change before we can continue doing anything. What's important is that you obtain this URL. And now you have to go back here before you can try anything and go inside of settings and environment variables and find the next public app URL. This is the one we have to modify. Otherwise, our API will not work at all. And you have to paste your deployment app. So I copied it from the sign in. So I will now remove it. There we go. Like this. No forward slash nothing. And click save. And now you have to redeploy. So go inside of deployments here, select the last one and click redeploy like this. And while you are doing this, go ahead and modify some of your OAuth providers here. So you can go back to the cloud console here, go inside of credentials here, web client one, and let's see what we can modify. So for example, we allow HTTP localhost, but let's also add this one. So let's click save. So this Vercel URL should also be able to trigger this credential login. Let's go inside of OAuth consent screen here. Let's click edit app. And there we go. So authorized domain has already been added here. Perfect. We don't have to do anything. And now we have to do the same thing here inside of our GitHub OAuth. Let's go ahead and change the homepage URL to be this and click update application. And now both of those should work here. So let me just quickly check, is my building finished? It's still not finished. So I'm going to pause the screen and see when it finishes. And there we go. Looks like it is successfully deployed. So how about we go and try our app again? I'm going to refresh here. And I will now try logging with Google purposely. I want to see if that's working now. or not. So once I clicked on login with Google, I got this app right exception invalid redirect right here. And I kind of think I know why this is happening. Let's go inside of source, inside of, let's see, I think it's inside of my lib or out here. It's using origin. I'm not sure if I should be using this. I think that we should be using process.environment next public app. Basically, our localhost 3000 here or when we deploy the actual app that we have added. So I will comment this out for now and make it work like this So let me just check inside of my app or out here Do I use any origin here I don Next. Well, what I can do is also, you know, hard code it here. Let's do that process.environment next public app like this. How about we hard code it like that so we don't go away from our application? let's see if that will work. So what I'm going to do is I'm going to git add, git commit, fix out in prod, and do git push. And what this will do is it will automatically trigger another deployment here. So if you go inside of your project here, deployments, there we go. Another building deployment. Let's try that out. All right, so after some debugging, I have found our issue. First thing you have to do is revert the change we just added. So don't uncomment this and bring back the origin here. This is inside of your lib or out. So both for sign up with Google, use the origin from headers like this. So exactly as we did the first time. No changes needed. For the route, same thing. Bring back request next URL dot origin. After you have made those changes, go ahead and do git add, git commit, edit revert changes, and then git push like this. And this will re-trigger another deployment. And once you do that, it will still not work. You can see I am clicking here, and what's happening is I'm getting some errors in my Vercel logs. What you have to do is you have to go back inside of your AppRite console here. You have to go down in the integrations, click on platforms, click add platform, web app. Select production for the name and select asterisk.vercel.app and click next. And you can click skip optional steps. And now you have the production which allows Vercel. And if you try now on your production URL to log in with Google, for example, there we go. No more errors. And I am redirected here and I have my name. Perfect. And if I, for example, log out and if I try logging with GitHub here, same thing will happen. But well, interestingly, it connected the accounts, right? So my Google account now became my GitHub account. That's interesting. Great. So you have successfully deployed, fix all the bugs and have a working application. Thank you so much for watching this very long tutorial and see you in the next one. before wrapping up one thing we should take care of is proper invalidation of our analytics because right now if i were to let's say create a new project so this is a new account that i have set up if you have an existing project that is fine but right now what happens if i go ahead and create a random task here let's just wait a second what happens is that my analytics is not updated immediately. And same is true for my workspace analytics. As you can see, it does get updated because this is the first time it was fetched for me. But back on my project ID page, it doesn't get re-invalidated. So let's go ahead and do the following. Let's go inside of source. Let's go inside of our features, specifically in the tasks API folders. And what I want to do is for every bulk update, every create, every delete, and every individual update, I want to reinvalidate two queries. So let's pick a random one. For example, use update task. Besides reinvalidating the tasks and tasks, I also want to reinvalidate the product analytics and the workspace analytics. I will just double check that these actually exist. So let me just check inside of my get product. My apologies, not product, it's project. Yes. That's why it's not appearing anywhere. So let me double check. There we go. I have that query key right here And for the workspace analytics I also want to double check There we go I have that query key right here And now I will just copy these two queries here from use update task and I will save this. And I will go inside of use delete task, and I will simply add those two here. I will close and save use delete task. I will go inside of use create task, and I will do the same thing here like this. And I will close this and I will go inside of use bulk update tasks. And I'm going to do the same thing here. There we go. So now what will happen is if I refresh this, I will get the new analytics because we are now fetching. But if I were to create a new task this time. And let's do something. Let's put it inside of Done. Let's add Create. And there we go. You can see how now one completed task, two assigned, two total tasks. And inside of the homepage here, we should get the exact same thing. Perfect. And if I were to go inside of a random project, I'm sorry, random task here and delete it, this should also affect this. Let's wait a second. And there we go. You can see how it's updated. Not immediately, but it will reinvalidate. There we go. Great. So that's one thing I wanted us to do. And then what I want to do next is go inside of your features alf and inside of our API use logout. What I want to do here is very simply just this. use just this, invalidate queries. And we can also leave router refresh inside of these ones. I think I've mentioned that before. But I specifically just want to call invalidate queries. So when you don't specify which queries, this will invalidate all of them. So the next user that logs in will not experience anything from the old user, right? And one more thing I want to add. Instead of our source app folder, dashboard workspaces, actually just dashboard page here, we use this get workspaces, right? And if a workspace exists, we redirect it to that workspace. Otherwise, we redirect to slash create. So what I'm worried about is if this caches and it remembers that we don't have any workspaces. So even though I've never experienced this, so every time I created a workspace, the next time I joined this, it redirected me to the correct ID. I think just for, you know, for a sanity check, I think it pays off to have it sorted inside of your features, workspaces, API. Once you create a workspace, what I would like to do is I would like to add use router here from next navigation. And I will allow this one to call router refresh on success. Purely because you can see that we are invalidating the query workspaces. So everywhere else where we are fetching the workspace's client side, we will 100% have that new workspace visible. But on this one, this is not re-invalidated by that. But instead, it's re-validated with router.refresh. And since this is a pretty important action in our page, I kind of want to take care of that. One drawback of this is that we will have this uneasy loading when we create a new workspace. But I think that's okay, because changing the whole workspace is a pretty, you know, big process. You can see it doesn't even look half bad, right? So now if you were to go ahead and log out, and let's go ahead and do like a new user, mail.com. I have to sign up. So I'm going to create a completely new account here. And let's add a new user, one, two, three, four, five, six, seven, eight. Let's register. And now inside of here, if I create a new workspace, I think the experience should be exactly the same. Nothing much here should change. We just confirmed the entire query is re-invaluated. That's great. And if I go back to localhost 3000, I should get redirected to this new workspace. Amazing. That's the last of the things I wanted to implement. Amazing, amazing job.",
  "transcript_chars": 345431,
  "transcript_filled_at": "2026-06-06T15:35:58.630526+00:00",
  "transcript_filled_by": "tk-bulk-groq-retry-20260606"
}