So now let's go ahead and let's fix this 404 page but just before we do that I forgot about one thing that I want to do and that's when we are in a specific organization I want to display the name of that organization in the tab right here. Currently it only says Taskify right? So let's go ahead and see how we can do that because we're gonna do the same thing when we click on a specific board but first I want to do it for the current organization. So Let's go inside of the app folder, platform, dashboard, organization and organization ID and in here we have layout.tsx. So first thing I want to do is simply go inside of my terminal and I will just shut down the Prisma Studio and write npm install lo- like this.
And I believe we also need to install the types for that. So let me just try and import x from lo-. Yes, we also need the types. So get back in the terminal and after you do lodash do npm install npm install dash d add types slash lodash as well so we have the types and that's it we can close this and there we go and from lodash go ahead and extract a start case and now let's also go ahead and create another function here export asynchronous function generate metadata function generateMetadata() and go ahead and extract organization slug from auth from clerkNext.js so let me just move that here and let's go ahead and return title to be start case organization slug or we're just gonna put in organization like this great And let's save this and let's see if it's working and there we go. Take a look at my tab now it says another and we have a pipe and then it says Taskify.
If I switch to a new organization Now it says test pipe taskify. So how does it generate that pipe taskify? How does it know that? Well, if you revisit our main layout here, you can see that we created a template, right? So when we add a new title inside of a layout, inside of a different layout, which is not the root layout, it keeps the title, but it moves it, you know, here to the side and we actually fill only this variable so it keeps the pipe in between.
Perfect. So now what I want to do is I want to create this 404 page right here. So as you can see the URL for that is a slash board and then a specific ID. So let's go inside of the app folder here inside of dashboard and in here create a new folder called board and then create another folder and this time it's going to have dynamic id so board id like that and let's go ahead and write page.csx inside and let's do const board id page and let's go ahead and return a div saying board id and don't forget to export default board id page great so once you save that you should no longer be having any errors, but it should also be a completely blank screen, right? And you still have the ability to do that from here.
And now if I'm not mistaken, when you create a new one from here you should also get the redirected yes as you can see my redirect is working as well my url changed but still this is the page that well has no content inside currently and the reason the board id is not visible is because it is hidden inside of this navbar here so we're going to have to move it inside of our layout. So let's just leave the board ID page like this for now and now I want to go ahead and actually create our board ID layout. So inside of board ID create a layout.csx like that and let's do const board ID layout and let's extract the children and we can immediately map the children to be react.reactNode and let's just return a div here which renders the children Let's export default board ID layout and after you save there should be no more errors and now let's go ahead and let's add a main element here around the children and let's give it a class name of relative and pt28 and h-full and there we go now we can see that board id text here and now what I want to do is I want to fetch the current board by id So let's go ahead and do the following.
Besides the children we're also going to have access to the params because remember layout is a server component. So every server component including layout also always has access to params. So let's add params to the types here and we know what it's going to be inside. There's going to be a board ID which is a string. How do we know that?
Well, because this board ID matches exactly what we named our folder. So ensure that you have no typos and I wrote a board ID with a capital I. So that is also very important if it's lowercase you're not going to be able to access it like this then you're going to have to be able to access it like this instead. So make sure that you keep track of cases and everything. Great.
And now let's go ahead and let's get our organization ID from ALF and we did that using clerk-next.js. If there is no organization ID we can just redirect. So we do that from next slash navigation and just redirect to select organization. There we go. Make sure you have next navigation imported.
And now what I want to do is fetch the current board. So const board is await DB. I imported that from add slash lib DB. So let me just move it here. And Since we use await, it means we have to make this an asynchronous component.
So await database.board.findUnique where id is params.boardId and the organizationId is the one which the user is currently using. Great, and if there is no board, this is a cool thing that I just recently discovered in Next14, you can manually trigger a 404 just by using not found which you can import from navigation just like that you redirect to the not found page so there we go and we can of course style the not found page but you know we'll leave that at the end if we have time to do it. Alright, so make sure you import not found and now let's go ahead and give this div a style to be background image, open backticks, URL and go ahead and use that board to get image full URL. So this is where we're going to use that full image which we stored and give it a class name of relative, h-full, bg-no-repeat, bg-cover and bg-center like this and there we go. Now we can see our beautiful image here and let's try and click on another image here, There we go.
You can see how all of our images are now loading in their full size after we click on a specific board. Perfect. So exactly what we wanted. What I want to do now is that when I click on a specific board I want to change the name to well the same thing I just did with organization right I wanted to say test in the tab right here so the user knows that that's the board that they have So we're going to do that in the board ID layout as well. So let's go ahead and do export asynchronous function, generate metadata.
And metadata, of course, also has access to the params. So let's map them. Params is an object which holds the board ID which is a string and it's not an arrow function so just open it like this and let's go ahead and extract the organization ID from Auth, which we already have imported. If there is no organization ID, in that case, just return title to be board. So a generic title, nothing more, nothing less.
And then let's attempt to fetch the board. So const board is await db.board.find unique. And it's the exact same query we just did below. So id is params.boardId and orgId. And let's just return title to be board?.title or just board generic as we did above.
And there we go, you can take a look now and in your tab you should see the title of your exact board so if I click on test 1 2 3 my tab now says test 1 2 3 perfect great great job so that's what I wanted us to do what we're gonna do next is we're going to whoops What we're gonna do next is we're going to, whoops, what we're gonna do next is we're gonna go ahead and create a little navigation bar here so we're gonna be able to rename the board if needed. So inside of the board ID layout here we're going to add a new component called board navbar. So just above this main element which is wrapping our children We're going to add a board navbar like this. Whoa, I completely butchered that, sorry. So board navbar like this and if you save you of course we're going to get an error because board navbar does not exist.
So let's go inside of the board ID folder and create another underscore components folder inside and let's create the board-navbar.csx and in here let's go ahead and let's write const board navbar actually export const board navbar and let's just return a div saying board navbar. Like that. Go back to the board ID layout and now import that from ./.components board navbar and when you save you should no longer have any errors. What I want to do before we head inside of the board navbar is actually just darkening this background a little bit so it's great that we can see it. But I just want to darken it a bit because we're going to have you know lists here and the lighter the image the lesser it's actually going to be visible.
So let's go ahead just below this board navbar create a div element which is going to be a self-closing tag and give it a class name of absolute inset0 and bg-black-10. So we just darkened it by a little bit, it's not even noticeable alright and now inside of this board navbar I want to go ahead and just define the actual props here, so interface board navbar props and let's go ahead and let's accept the ID to be a string so let's go ahead and do the same thing here let's extract id and let's write board navbar props like that and then let's just go ahead and pass in the id to be params.board id because the board navbar is still a server component so we can actually repeat this fetch from here so let's go ahead and mark this as an asynchronous function let's go ahead and import the database util from s-libdb and in here let's go ahead and import that but this time we can immediately get the id from the props and we just have to find a way to get organization id and lucky for us that's very easy just using ALF like this and let's go ahead and make this org ID like this the reason we can do that here is because we're gonna redirect already here if we don't have the organization ID so no need to do that otherwise.
Great and now let's go ahead and let's give this div a class name of w-full height of 14, Z 40 pixels, sorry 40, BG black slash 50, fixed, top 14, flex, items center, BX dash 6, gap dash X dash 4 and text dash white. Great so now as you can see we have a nice little navbar below our first navbar here. And actually I noticed that we are kind of repeating a lot of stuff here. So how about we change that? I mean there is no reason that we cannot pass the existing board directly in here.
So let's actually do that. I think it's kind of going to save us some time. So let's go ahead and pass in this board here. And let's go ahead and accept the data to be board from Prisma Client. And then we are working with data, right?
And then we don't need any of this. Great I think it's just a bit simpler. All right and now let's go ahead and let's create a component which is going to properly render the title of which is this board and when we click on it it's going to allow us to rename the board so we're also going to have to create the actual server action for that. So we're going to do that here inside of a board navbar. So let's create a component called board-title-form.
So it's going to be in the same level as this board navbar. So let's write board-title-form.csx like that. Let's go ahead and let's mark it as use client. Let's export const board title form. And let's go ahead and let's return a button component like this.
And let's immediately create an interface for it board title form props and inside of here we accept expect the data which is a type of board from Prisma client So let's immediately assign those props here. Word title form props. We extract the data and then inside of this button let's go ahead and let's render data.title and let's go ahead and give this button a class name of font-bold, text-large, h-auto, w-auto, p-1 and px-2 And let me just reorder my imports a bit. All right, let's save this and let's go back inside of the board navbar here and instead of this text here let's render the well the board title form from. Slash board title form make sure you have that imported here and let's pass in the data.
Great and now you can see I have a big button here which says test which is the name of my board and now I want to go inside of the button component so I can add a new variant which is going to be more suitable for this kind of transparent mouth bar here. So let's go ahead inside of components UI and find the button.tsx and just below this primary type of variant or whatever is your last one go ahead and add a new one called transparent. We're gonna give it a BG of transparent, text is gonna be white and on hover BG is gonna be white slash 20 and now go back to board title form which is inside of your board ID components board title form and give this button a variant of transparent and there we go now it looks much better it's flush with the navbar but when we hover there is a very obvious action which we can do here so now let's actually make it do something so I'm gonna go ahead and import useState from react Let me just move it here and let's go ahead and let's add a constant isEditing and set isEditing here to be useState and by default let's give it a false value.
And now I want to create a constant disableEditing which is a function which is very simply going to set isEditing to be false. Great and now let's go ahead and let's create an equivalent enable editing and I'm just gonna add a quick comment here to say to do focus inputs but since we don't have any inputs at the moment let's just do set is editing to be true. All right and now let's go ahead and give this button an on click to enable editing. And now in here let's write if isEditing in that case we're gonna go ahead and actually render a form so return a form element and let's just leave the action empty for now and let's give it a class name of flex-items-center-gap-x2 and let's add the form input component from add slash components slash form form input like this and let's go ahead and give it an ID of title let's give it an on blur on blur well actually it's just an empty arrow function for now but we're gonna have some blur action here happening let's give it the default value to be board.data.title and let's give it a class name to be text lg font bold tx-open brackets seven pixels py-1h-7 bold px-7px py-1h-7 bg-transparent focus-visible outline-none, focus-visible is going to have ring-transparent and we're going to have border-none.
Great, and now let's go ahead and let's create a couple of refs here. So right here at the top I'm going to add const form ref to be use ref from React and also element ref from React. So make sure you have element ref, use ref and use state from react and let's give this first type of element ref to be form and by default it is null and then let's add const input ref to be use ref again element ref a type of input and null And now let's assign this to refs properly. So the form ref is obviously the form ref and the ref for the input is input ref. Great!
And now let's go ahead and let's create let's modify this enable editing here to actually focus right so I'm gonna add a little set timeout here and let's do input ref dot current focus input ref dot current select and let's go ahead and try this out now so when we click here there we go you can see how we switched to an input type. Perfect. Now let's just go ahead and prepare the form for submitting. So I'm just gonna go ahead and create a new constant on submit. We're going to get the form data, which is a type of form data.
And let's go ahead and just console.log I am submitted. And we can also immediately extract the title to be formdata.getTitle as string. And let's log that as well. And now we can pass that to be the action of our form. Great.
So let's try it out. I'm going to open my console here and when I write 1, 2, 3 and press enter, there we go, it says I am submitted 1, 2, 3. Perfect. And now it's time for us to actually go ahead and well create the actual form action. But just before we do that I just want to fix this little like an outline which we have visible here.
So I mean if you like it you can leave it but I think in the demo I didn't have any outline so I just want to show you how to resolve that. We have to go inside of the components folder inside of UI and find the input from ShatCM and in here we have the focus visible ring offset and let's go ahead and change it from 2 to 0 and I think that once we modify that, there we go, the offset is no longer visible. And while we are here, let's also change the rounded MD to be rounded SM like this. So this way it just fits more to what Shazian actually, not Shazian but the Trello actually does. Perfect.
So now we have this kind of flushed experience when changing a board. There is no difference between the original one and this one. Very nice. Great. And now let's go ahead and let's actually well just wrap up this board title form on blur here right so when it when on blur occurs what I want to do is I want to manually trigger the submit as well so we can do const unblur form ref.current request submit so that's gonna trigger this function then And let's go ahead and just pass it here.
And I'm going to go ahead and open my console. So I just want to give you kind of a neat experience here. If user types something, goes outside, I want that to save, right? Because that's how Trello behaves as well. Perfect.
So we have all of that set up and now we are actually ready to create our server action, which is going to modify this new title from here. So I'm gonna call that action update board. So let me close everything here, let me go inside of actions and let's create a new folder update board and we can actually remove this delete board old server action which we have and inside of this update board first let's create schema.ts so inside of schema we're going to need to import Zod of course. And then let's define export const update board to be z.object and inside all of the inputs we expect. So obviously we expect the title which is a string, it has a required error of title is required and it has an invalid type error and I'm just gonna say title is required as well for this one and let's also keep the minimum value of 3 to stay true to the creation of the board.
Title is too short in case this happens. But besides the title we also expect the user to pass in an id which we're just going to define as a type of string. So using the ID, we're going to confirm which board does the user want to update. And now let's go ahead and let's create the types. So go ahead and types.vs here.
And again, let's import z from Zod. Let's import board from Prisma slash client, let's import action state from lib create safe action and let's import update board from dot slash schema and now we can export type input type to be z.infer whoops z.infer type of update board and export type return type to be action return type to be action state input type and board. Great! And now let's finally go inside and create index.ts. Let's mark this as use server and let's go ahead and write const handler to be an asynchronous function which accepts the data which is a type of input type from ./.types and returns a promise which gives us a type of return type again from ./.type.
And let's go ahead and return this arrow function here and let's extract the user ID and organization ID from Auth using clerk-next.js and I will just separate those two here in the inputs. Let's check if we are missing a user ID or if we are missing the organization ID. If either of those is true, let's return an error saying unauthorized. Great, and now let's go ahead and let's extract the title and the ID from the data and let's write let board here just so we prepare it and let's open a try and cache block here. So inside of the try block, let's assign the board to be await DB which we can import from a slash lib DB I'm gonna move it here so await DB dot board dot update where ID matches and of course organization ID matches as well and let's use data to be title.
So using this organization ID we have prevented anyone from outside of this organization to update this board. So only the person from, which has the proper organization ID inside of the auth util, which cannot be mocked or faked in any way, can actually update this board. So if you watched my previous tutorials, this is similar to checking that the current type of user which is trying to update this entity has a matching user id but since this time we are not working with user ids we are working on an organization base so that's why we are checking for the organization ID so this is a kind of security for that. Great and now let's cache the error here and let's just do return error failed to update. All right and now let's go ahead and let's revalidate our path.
So import that from next slash cache. And Let's revalidate the path to be slash board and then the individual ID which we extracted from the data. So it's immediately updated. And let's return data to be the board. Perfect.
And then let's export const update board to be create safe action passing the update board schema so you can import that from ./.schema let me just move it here so make sure you have create safe action from ./.lib create safe action and schema and in the second argument pass in the handler. Great! And now we can go back inside of our board title form. So let's go ahead and find where that is. So inside of the app folder, platform, dashboard, organization, organization ID, sorry, board, board ID components, board title form right here.
Let's go ahead and import that action. So update board from actions update board and let's also import the use action from hooks use action. All right and let's go right here. I'm going to do this at the top level here. Let's extract execute from use action and let's pass in the update board and let's go ahead and extract the on success here we get the new data and inside let's call the toast which we imported from Sonar I'm gonna move it to the top here.
So toast.success. And let's immediately say, so open backticks, board, open annotations, and we're gonna spell out the exact title of this board. So data.title, so board, the title of the board, updated. So the user can immediately see that we successfully reflected those changes, right? And let's go ahead and call the disable editing here.
Great. And now let's go ahead and use this execute. So I'm gonna go inside of the on submit here and let's go ahead and let's call execute let's pass in the title and pass in the id to be data.id like that great and just before we try it out one more thing that I quickly want to do. I want to keep my title inside of state rather than directly accessing it from data. So we can do optimistic updates on it.
So go ahead and write const title setTitle useState by default to be data.title and then let's just find if we use data.title anywhere we use it right here in the form input so scroll to where we have the if is editing clause and change the data.title to be the title from state and also same thing in the button here make sure it uses the title from state and now what we can do is inside of here on success before we disable the editing let's do set title to be the new data dot title great and let's also extract on error get the error and let's do toast.error the error. Great and I think this should pretty much be it. Let's try it out now. So right now it's called test and now I'm going to change it. I'm going to press enter and you can see it's pending and it says board change it updated and if I refresh the name stays.
So another change here and this time I'm not going to press enter instead I'm going to blur and there we go same thing board another change updated. Perfect and now what I want to create is a little action here on the side which is going to be used to delete the board. So let's go back inside of the board navbar component. I'm going to close everything here and let's find board navbar. So it's located inside of app, platform, dashboard, board, board ID, components, board-navbar.
And in here just below our component board title form which we just wrapped up let's add a div with a class name of ML auto so this way we push it all the way to the right and inside let's render board options and let's pass in data actually we're only gonna be needing the ID so we can pass in the ID to be data.id and let's go inside of the underscore components and immediately create board options.tsx Let's mark them as use client and let's export const board options here. Let's extract the id. Let's create a quick interface board options props to accept the ID which is a string and let's just map those here and let's just return a div options. Go back to the board navbar and now you can import the options from ./.boardoptions the same way we did with the title form and you should no longer have any errors and you should see the options text in this second navbar here. And now let's go ahead and let's import everything we need from the popover component so import from s slash components UI popover we're going to need the popover itself We're going to need the popover close, the popover content and the popover trigger.
Great. And now let's go ahead and let's change this entire thing to be a popover. Let's add the popover trigger here. Let's mark it as a child. Inside let's add a button component which we can import from add slash components UI button.
Make sure you add that. And let's simply render more horizontal from Lucid React as I added right here but I'm just gonna separate it here at the top. So more horizontal is an icon and let's go ahead and give it a class name of h-4 and w-4 and let's give this button a class name of h-auto and w-auto and padding of 2 and a variant of transparent so it fits the look of our invisible navbar here. There we go. That looks quite nice.
And now outside of the popover trigger let's add the content. Now this content right here has the class name of Px0, padding top of 3, padding bottom of 3, and a side of bottom and a line of start. So let me just collapse this. So it's nicer to look at. Great.
And inside of the popover content, let's go ahead and create a div which is going to say world actions. And inside of here, let's give it a class name, text small, font medium, text center, text neutral 600 and padding bottom of four and I think we can already take a look at it so if I click here there we go we have a nice text which says board actions. Now let's go ahead and let's add the close button for this popover. So after this div here add the popover close. Let's give it a prop as a child and inside open up a button again and render the x icon from Lucid React so just make sure you add that import.
Whoa, okay it's a self-closing tag Let's give it a class name of h4 and w4. Let's give its button a class name. Let me just collapse so we have more room. H-auto, w-auto, padding to absolute, top-2, right-2, and text-neutral-600. And let's give it a variant of ghost.
And now we should have the close button visible. There we go and it works. Great and now let's go ahead and render some options for us here so after the popover close let's go ahead and let's add a button element again and let's write delete this board and let's give it a variant of Ghost. Just let's not misspell variant. And let's give it on click for now to just be an empty arrow function.
And let's give it a class name of rounded-none, w-full, h-auto, p-two, px-five, justified-start, font-normal and text-small. Like that. And we should have a nice little action visible now which says delete this board. And now let's go ahead and let's create an action, a server action to actually delete our board. So we can do that quite easily because we have existing actions here.
So I'm gonna reuse this one to update the board. So I'm going to copy the entire folder and paste it inside of the actions folder and I will rename the folder to delete board and first let's go inside of the schema to change what we need. So previously we needed well the title right but this time we only need the ID so you can change it to this and let's change this schema to be delete board. Now let's go ahead inside of our types.cs and let's change the input type to be delete board. And we also have to modify the type of here.
The rest can stay the same. Now let's go back inside of index and let's go ahead and import delete board from here and scroll all the way down and add it as the first argument in the create safe action and looks like I have a typo so it's delete board and let's also rename the actual function from update board to delete board. Great! And now let's go ahead and let's remove the title from here we don't need it. The authentication stays the same and now what we're going to do is we're gonna call db.board.delete and we are not gonna be passing any data inside.
Instead we're gonna try to delete the board which has this ID and the organization ID which the currently logged in user has. So if someone else is trying to do that we're not gonna allow if they are not from the same organization. And instead of fail to update let's say fail to delete. And now let's revalidate the path not board but let's go ahead and do slash organization and then let's use the org ID and we're not gonna return anything instead we're going to redirect so call that from next slash navigation I'm just gonna move it here so we're gonna be using the redirect and let's go ahead and let's go to slash organization slash organization ID like this. Perfect!
So now that we have this action let's head back inside of the board options where was it? Right here and let's import everything we need so we're going to need the delete board from actions delete board we're going to need use action from hooks use action inside of here let's go ahead and let's define that so we're going to extract execute and isLoading from useAction let's pass in the deleteBoard option let's modify the onError callback If we have an error we're gonna call toast which we can import from Sonar and let's call toast.error and let's directly log that error and I'm just gonna move this sonar to the top here. And the reason we're not going to be doing anything with the onSuccess well, there's no need, right? Because we're going to directly redirect, as you saw in the action, back to the organizations page. So the user is clearly going to see that the deletion was successful.
And now let's go ahead and let's create the onDelete handler. So const onDelete, execute and pass in the ID. And now let's go ahead and get this is loading and let's make this button disabled if it's loading and let's change this empty on click to be on delete. Great! So now let's go ahead and let's just confirm this.
So inside of my Taskify I have a board called another change and let's go ahead and delete it from here. It's loading and there we go it is no longer here. Perfect! So we successfully finished our initial options for manipulating the board, creating the board and deleting the board in the end. Great, great job!
What we're gonna be doing next is, well, finally rendering some lists here and some cards.