So let's go ahead and let's create the actions for each board so that we can copy the board ID, delete the board and also rename the board. In order to do that I want to add an element from ChatCN UI called drop-down menu. So let's go ahead and run mpx chatcn-ui-at-latest add drop down dash menu. This is how it looks like in one line. So go ahead and add this.
And while we are here, we can also add an alert dialog component and we can also import the package we're gonna need so let's do that as well. So let's also add alert dialog which we are going to need and then we're gonna install zustand. So npm install zustand or zustand. I'm not sure how to pronounce that. So make sure you have drop down menu, alert, dialog and zustand.
And then let's do npm. Actually, I think I already have it running. Let's see, npx-convex-dev and npm-run-dev. Great, I have both of those running. So I can just refresh this to confirm that.
There we go. So now let's go ahead inside of our app folder, inside of the dashboard, inside of our components. I want to go ahead and... Actually not here because these actions are going to be reused in the board screen as well. So let's create them in our global components folder right here.
So inside of here go ahead and create actions.tsx. Let's mark this as use client and let's create an interface action props. Actions props like this. And let's go ahead and define the children to be react react node let's add an optional side which is going to be drop down menu content props which you can import from Radix UI. And let's go ahead and let's get the side prop.
And let's go ahead and do the same thing for the side offset and just pick the side offset prop. So you can import this from Radix because we added that using Shazzy and UI. So we are only importing the props from Radix. We will not be using the actual component from Radix. For that, we're gonna use UI components drop-down menu.
And now let's go ahead and give it an ID of string and let's give it a title of the string. Great. And let's go ahead and create export const actions. Let's destructure these props. Let's extract all of them.
So children, side, side offset, ID and title. And let's very simply just return a div saying actions. And now let's go ahead inside of our app folder dashboard components board card index.tsx right here and we're gonna go ahead and put this just below the overlay we're gonna add our actions component from components actions So this is how I imported that. You can see that I put it in our global reusable actions folder because we're gonna use this twice in the project. And let's go ahead and pass in the ID for this board.
The title is the title. And let's also add this side to be opened on the right. And we are missing the children and for that let's go ahead and do it like this. We're gonna pass in a button component And then inside I'm just gonna write I am a button for now. And now I believe this won't even be rendered anywhere.
I don't think this is even visible. Let's just go ahead. So we have this div which says actions but I'm not sure if we should even be seeing this let's go ahead and let's give this a class name absolute z50 top one right one there we go okay so we can see it that's what I just wanted I just wanted to make sure that we can see this. Alright, so what we're gonna do now is we're gonna go ahead and actually render this children right here. So this button which we are supposed to see.
So right now we are not seeing that, we are seeing this text actions. So we're going to go ahead and import everything we need from the actual components UI drop-down menu, but not from Radix, from our components. So we're going to need the drop-down menu, the drop-down menu trigger, the drop-down menu content, and drop-down menu item, and also drop-down menu separator. So let's wrap this entire thing inside of a drop-down menu and then let's add a drop-down trigger inside where we're gonna just render the children and let's give it an as child props so it actually shows that button over there and we can leave it like this for now and now what we have to do is actually style the children which we are passing inside of the actions because this is where that is going to be rendered and as you can see we cannot see it anywhere now but I want it to appear right here. So let's go ahead and give this button some class names and before we do that let's simply add a little icon from Lucid React.
So we're going to use an icon more horizontal from Lucid React. So import this icon and render it inside of this button right here. Let's give this one a class name of TextWhite, Opacity 75, hover opacity 100 and transition opacity. And I believe this still won't be visible because we have to style our top button here. So let's give this button a class name of absolute, top1, right1, opacity 0 and Group hover opacity 100, transition opacity, PX 3, PY 2 and outline none.
And now let's check that out and there we go. You can see how we now have an appearing more horizontal icon button right here. Of course, when I click on this, I'm redirected to a 404. We're going to use prevent default and stop propagation to fix that in a second. But once you hover on individual card, you should be seeing these three dots and when you hover over them directly they should be even more visible because we change their opacity.
Great! So we are done with this part and now we can focus exclusively in the actions here. So let's go ahead and let's create the drop down menu content which will open once we click on this. So drop down menu content. Like that.
And let's go ahead and give it a side of side. Let's give it a side offset of side offset. Let's give it a class name of width 60, like that. And I want to add on click here and get the event and anywhere where I click inside I'm going to stop propagation so that I'm not redirected once I click on this part of the component. And now inside let's add a drop-down menu item here.
And in here I want to render a link to icon from Lucid React. So just make sure you add this import I'm gonna add it here. Let's give this link to icon a class name of height 4, width 4 and margin right of 2 and a text is gonna be copy board link and let's give the drop-down menu item itself a class name of Padding3 and Cursor Pointer. So let's see if this improved anything. So now when I click here, there we go.
You can see I have an option to copy the word link and I can click here and I can click here and I'm not redirected at any point unless I actually click on the card then I'm redirected to a 404 page but if I just go ahead and click on this little toolbar I'm not redirected And we achieve that by using onClick event stopPropagation on the drop-down menu content. This is of course my solution for this. If you know any better solution to stopPropagation from the link, feel free to use it, feel free to leave a comment if you think there is a better way to do it. Alright. And now let's actually implement this functionality so that we can copy a link.
Since that is going to be very simple, we don't need any mutations for that. So const onCopyLink is very simply going to call the navigator.clipboard.writeText. And very simply, we're going to go ahead and do the following so we're gonna go ahead and call window dot location dot origin slash board and then the individual ID. And then we can add .then here because this is actually a promise. So in here we can call our toast from Sonar.
And let's write a success link copied and let's also do a catch even though this can very rarely fail I believe failed to copy link so I imported toast from the sonar package So just don't forget to do that. And let's now add the on copy link here to this dropdown menu item. There we go. So now I believe this should already be functional. So if I copy this board link and let me paste here there we go I have a working id and I have a toast that link has been copied if I try this one you can see that it's a different id perfect so this is working What I want to do now is I want to create the delete functionality.
So the first thing I want to do for that is go inside of the convex folder and go in here in the individual board function. So let's go ahead and do the following. Let's go above the create here. Actually, let's go to the bottom here and let's do export const remove, which is gonna be a mutation. Did we import a mutation?
We have a mutation, all right? So that's gonna be a mutation, which accepts the arguments, which are now just gonna be ID, which is a type of ID and boards. Then we're also gonna get the handler itself, which is gonna be an asynchronous function, which has context and arguments. And then in here, we're gonna get the identity and we're gonna do await context out get user identity and let's check if there is no identity. In that case, throw a new error.
Unauthorized. Like that. And then what we're going to do is await context database delete and very simply we're going to pass in arguments.id. So we don't need to specifically say to delete from the boards because the id is type of id boards so convex knows that we are deleting the boards schema so for now we're just gonna do it like this, but I'm gonna add a little to do here. Later check to delete favorite relation as well.
So when we add favorites later, we're gonna have to remove that user relation as well. Otherwise we're gonna have some bugs. But this should work just fine for now. So we can go back inside of our actions. So it's located in our global components folder right here, actions.
And let's go ahead and let's add another drop-down menu item. So I'm going to copy and paste this one. And I'm going to go ahead and use the icon Trash2 here from Lucid React. And the text is going to be Delete. Like that.
And let's go ahead and let's import. I'm going to use my use API mutation hook from hooks use API mutation here. So I'm going to go ahead and extract that. So const right here mutate and pending. It's going to be use API mutation API from convex generated API dot board dot remove.
So The reason I'm calling it remove is because I don't want to take the reserved delete keyword in JavaScript. So make sure you import the API from s slash convex underscore generated API. You can use my use API mutation or I already showed you, you can use useMutation from convex.react and then you have to write your own pending if you want to do that. Both will work. And now we can add a function const onDelete which is very simply going to call mutate passing the ID which we have, poll.then post success, board deleted, and .catch post error, failed to delete board.
Like that. So let's go ahead and copy this and let's add it instead of this one since we copied that from the one above. So let's change it to a proper key. So now it should immediately be deleted once I click here. There we go.
Board has been immediately deleted. You can see how it is working. What I wanna do now is I wanna add a little confirmation model before this happens because you can see that it's very easy to make a mistake and it happens really fast and there is no undo. So, I'm gonna go ahead and create a reusable component called confirm model which we can build because we have the alert dialog. So let's go ahead inside of the components folder and let's create a new file confirm model.tsx Let's import useClient and let's go ahead and import everything we need from components.ui.AlertDialog and that's going to be AlertDialog itself AlertDialogAction AlertDialogCancel AlertDialogContent Description we're also going to need the footer, the header, the title.
Whoops. So alert dialogue title and alert dialogue trigger. So all of these components here are needed to build this let's create an interface confirmModelProps to have children which in our case is going to be our delete drop-down menu item so that's going to be react reactNode Then we're going to go ahead and have an onConfirm function, which is going to be an empty void. Disable prop in case we want to disable that from the outside. A header to display a different message if we want to, and description, which is going to be optional, to display a more specific message if we want to use that for something else.
So we don't need to use this for deletion. We can use this for whatever we want to confirm. And then let's export const confirm model here. Let's go ahead and extract this props. So confirm model props, children on confirm, disable, header and description.
And then simply inside, we're gonna go ahead and return the alert dialogue, then the alert dialogue trigger, which is simply going to render the children and have the as child property. And then we can use the alert dialogue content, which needs to be closed here. And inside let's add the alert dialogue header, alert dialogue title. And in here, we're gonna render our header prop, like that. And then just outside of the title let's add alert dialog description in where we're gonna load the description prop like that.
And then outside of the header, let's add a alert dialogue footer component. And in here, very simply, we're going to have the alert dialogue cancel, which is just going to say cancel. And below that, we're going to have the alert dialogue action which is going to say continue or confirm whatever makes more sense to you perhaps confirm would be better and now let's go ahead and let's give this a disabled prop of disabled and let's go ahead and give it an on click on handle confirm so we're gonna build handle confirm now which is gonna be very simple so const handle confirm is very simply gonna call the on confirm option like that there we go, that's it that is our reusable confirm model component So what we can do now is we can go back inside of our actions component. We can go ahead and import this. So I'm going to import confirm model from .confirmmodel or .confirmmodel, however you prefer it.
And then you can very simply just wrap the entire drop-down menu item in confirm model like that I believe that we can do it like this and what we have to do now is we have to pass in the title sorry, the header which is going to be delete board question mark. We need the description, which is going to be this will delete the board and all of its contents. And let's also give it a disabled prop of pending. And let's give it on confirm to be our on delete function, which we're using just below, like that. And here's what I want to do.
So I don't want to use the dropdown menu item because I believe this will now not exactly work. So let me just comment out this dropdown menu item on click. And now if I click on delete, you can see how the model opens and then closes very fast. So we cannot use the drop-down menu item as a child of the alert dialogs. Instead, what we're going to do is we're going to use the normal button here.
So you can just import button from .slash UIButton or components UIButton. Let's go down here and simply use that. As simple as that. But I believe we have to style this button a bit. So let's give it a variant of ghost and let's go ahead and give this a text small, a full width, justify start and font normal.
So it doesn't differ from our drop-down menu item component. And I think that now we're gonna have a nice confirmation flow. So there we go. The button doesn't really differ from the one above, but when I click here, you can see how I have a confirmation. If I cancel, nothing happens, but if I click confirm, then the board is deleted.
Perfect. So we just wrapped up the confirmation model. Now we have to do a similar thing but for the last option inside of these actions which is going to be the option to rename the board to something else. So we can do that by first going back inside of convex board right here. And we're gonna go ahead and create a new method called update.
So let's write export const update to be a mutation. Let's get the arguments to take the ID, which is going to be v.id words. And we are also gonna take in a new title, which is gonna be v.string. And then let's go ahead and write a handler which is an asynchronous function which has the context and the arguments so in here let's go ahead and let's get the title using arguments.title.trim Let's go ahead and check if there is no title, we're gonna throw newError and we're simply gonna say title is required. And then we're gonna check if titleLength is more than 60, then we're going to throw a new error.
Title cannot be longer than 60 characters. So that's why we are trimming it so that the user cannot pass empty whitespace as characters. And let's also get our identity. So identity is await context out get user identity. If there is no identity, let's go and do throw new error unauthorized.
All right. And now let's go ahead and simply do const board to be await context database patch. And in here, we're going to pass in the arguments ID, and then we're very simply going to update the title using the arguments and the new title and you can return the board. That's it we created our function to update the board and later we're going to go back inside of this remove and update function and we can further enhance the security of this. It will be how you want it to be so if you want to allow anyone from the organization to delete the board you can check whether the identity has the matching organization or if you want to you can simply check if the user that created the board is the only one that can edit it and the one that can delete it.
Or if you want even further you can modify so that only admins and the owners can delete that. So we're gonna play with that later. For now I just want a very simple update and remove functionality here. Okay, so now what we have to do is we have to go ahead and create a hook called useRenameModel. So this one is going to be different.
This is going to be a form model, which I want to control in a different way. More specifically, I want to control it using Zustand hooks. So let's create a new folder in the root of our application called store. And inside create use rename model.ts. And let's go ahead and import create from sustained.
And then let's go ahead and create default values, which are going to be an empty ID and an empty title. And let's create an interface, I rename model is open is going to be a boolean then we're going to have initial values to be a type of default values we're going to have on open which will accept the ID which is a type of string and a title which is a type of string and a title which is a type of string and that's gonna return a void and lastly we're gonna have an onClose function which is gonna return a void with no props inside and Now let's do export const useRenameModel to use the create. Let's pass in the IrenameModel interface here and let's get the set in the side of this double parenthesis here and let's open and immediately destructure an object inside so we can set the default is open to be false. Let's pass in the onOpen to get the ID and the title. And that's going to go ahead and call the set function in where we're going to call isOpen to be true and set the initial values to ID and the title like that.
And then let's go ahead and let's pass in onClose to call set again. And very simple, call the isOpen to false and initial values to be default values and last thing we have to pass here is the actual initial values to be default values. Like that, so now we have control over our model. So now what I wanna do is I wanna go ahead and create a basic model to rename the board. So for that let's go inside of components and let's create a new folder called models.
So in here, we're going to store those kinds of models, which are reusable throughout, and let's go ahead and create rename model.psx. If you want, you can also drag and drop confirm model inside, but confirm model is used differently. So you can see that the confirm model is used through this triggers, but the rename model is not going to be used like that. So it's going to be controlled with a zustand store, which we are going to programmatically open depending on how we need that. So, you know, your choice where you want to keep that.
So, RenameModel is going to be a client component. And let's go ahead and very quickly import everything we need from components UI dialog. So in here, I'm going to add a dialog, dialog content, dialog description, dialog header, dialog close, dialog footer and dialog title. And then we're going to go ahead and do export const rename model and as opposed to how we did it in the confirm model we are going to heavily rely on our user name model from store. So inside of here we're going to go ahead and extract is open on close and initial values and then what we're going to do is we're going to return our dialog and we're gonna pass in open to be controlled programmatically by is open and on open change is always gonna call on close so that will programmatically change this one to closed.
And let's add the dialog content here. Dialog header. Dialog title. Which is gonna say edit board title. And then let's go ahead and add a dialogue description which is going to say enter a new title for this board.
And I'm gonna leave it at this for now because here's what we have to do now. We could technically just put this in the root of our layout, for example. The problem is, in Next.js, when we use programmatic control of our models using something like zustand, that can create hydration errors. So I want to show you a bit of a practice that I do. So whenever I have multiple models like this, I create a model provider, so that I can safely add as many as I want and I don't have to repeat a specific piece of code every single time so we're gonna create that as well go inside of your providers where you have the convex client provider and add a model provider this is gonna be use client and let's import use effect and use state from react.
Let's import our rename model from components models rename model and export const model provider. And in here we are very simply going to return all of the models we are going to have. So In our case, this is just the rename model, but I'm showing you some practice that I usually do when I have a lot of models here, which all are programmatically controlled. So what I do is I create a little check here, is mounted, set is mounted, by default false, meaning that the rendering starts in server side, right? But this cannot be shown on server side, otherwise it's going to cause a hydration error.
So very simply, if I'm not mounted, I'm not going to render any of these models. But only once I get to the client side will I show them. So how do we know when do we get to the client side? Well, very easy. The useEffect can only be called if it's finally come to the rendering on the client side.
So if I just create a use effect with an empty dependency array and add set is mounted to true, this will ensure that this component is only ever rendered on server side. Because use client doesn't mean client-side rendering, it just means that it's not a server component. And server component, it's not the same thing as server-side rendering. Those are two different things. Components which are marked as used client are still server-side rendered.
They are just not server components. But this ensures that whatever I return here will only be visible completely on the client. And the only reason we do this is because if we don't, it will still work but you will get some cryptic hydration errors which are going to be very hard to debug unless you do this. So the reason I'm doing this inside of a model provider is so that in the future I can have as many of these types of models as I want and I can just easily add them here as opposed to adding this logic in every one of my models. So I can just do it once and add all of them here and then what I'm going to do is I'm going to go back inside of my app layout.tsx and just below the toaster I'm going to add model provider.
So make sure you add your model provider otherwise your rename model will never be rendered. So make sure you added the model provider in your root layout in the app folder. And now what we can do is we can call the onOpen function from the useRename model. So let's go ahead back inside of our actions. So actions from our components folder right here.
And let's go ahead and add that. So const onOpen from useRenameModel. So Make sure you import useRenameModel from store useRenameModel. And now let's go ahead and very simply find the third option here. So I'm going to go ahead and copy the drop-down menu item here.
Let's go ahead and let's paste it just below the copy board link and this one is going to be rename and the icon is going to be a pencil so make sure you import the pencil from Lucid react and on click is very simply gonna call an arrow function on open passing the ID of the board and the title of the board like that so right now what should happen once I click rename there we go you can see how I programmatically open up my board so you might be asking why did I go through all of this trouble? Why not just do the same logic as confirm model? You can go with that route as well, but I did have some issues with the behavior of the actual dropdown menu item and dropdown itself and how it renders and all the portals. Basically it all got messed up and I got some unexpected behavior and I didn't want to put that in the tutorial because I couldn't ensure that you wouldn't run into some weird issues with it. So what I did was I simply switched to programmatically controlling our model using Zustand.
I actually do that a lot in my tutorials so it might be even more familiar for some of you if you've been watching my tutorials. But you're Welcome to experiment yourself. If you want to use it the same way like I did with the confirm model, you can try and do that. Just remember you cannot use drop down menu item then. Then you have to use the button component.
And if you use the button component, then this drop down menu will not close when you click on it. So as I said, you will just run into a bunch of weird little issues. You can see when I click delete here, it doesn't close. The reason it doesn't close is because in here, in the delete, we use a button instead of a drop-down menu item. But this is alright for me because the confirm model will actually delete the entire object so then this will be closed anyway.
But for the rename, that's not gonna be true. So I want it to close immediately. So that's just one of the reasons why I switched to programmatically do this instead. Great, so just ensure that you can open your rename model and then we can go back inside of the rename model here and let's go ahead and actually write the logic for it. So I'm gonna go ahead here and let me just add my title and set title here to come from use state from React.
And I'm gonna pass in the initial values so I need to do that below the username model my apologies because we need the initial values so initial values.title like that let's add a use effect from react which are going to watch for the changes of initial values dot title. So if we open up a new one and we're gonna do set title here to be the new initial values dot title. Usually I really don't like doing this in UseEffect. The less we use UseEffect the better, but I'm going to make an exception for this edge case. So just make sure you have UseEffect and NewState imported from React.
I'm going to move that here to the top. All right, now that we have that, let's go ahead and let's add const onSubmit here to just be an empty arrow function for now. And now let's go ahead, just below our dialog description, Let's add our form element and let's add our input from ./.ui-input So you already have this component. We've used it for our search component so make sure to add the input component here and Let's go ahead and give it a disabled prop to be false for now, we're going to change that later to our pending state. Let's give it a native required prop.
We can also give it a max length of 60 because remember we do that on the backend as well. Let's give it a value of title. Let's give it onChange. We get the event and call setTitle of eventTargetValue and a placeholder of boardTitle. Like that.
And then outside of the form itself, let's add a DialogPointer component. My apologies, inside of the form. Like that. So make sure it's inside of the form we're gonna go ahead and add a DialogClose component which we'll simply say cancel and let's make sure this is as child because we're going to use a native button from chat-cn sorry not native the chat-cn button so just make sure you add this input like that and let's go ahead and style it a bit by giving it a type of button which is important so it doesn't submit to the form and the variant of outline and then we're gonna have a dialogue we don't need another one, we just need a button which will say save. This one is going to have disabled of false for now which we're going to change depending and a type is going to be submit.
So now if I try and click on one of this I should be seeing untitled right here and let's just give a little space in between these two buttons here. So I believe we can go directly to the form here and let's go ahead and give it on submit to our on submit which is an empty arrow function and let's give it a class name of space y4 like that I think that should already be better there we go great so we have that and I believe this will now be a controlled component which is working, perfect. So what I want to do now is I want to actually implement the onSubmit function. In order to do that I'm gonna go ahead and call my useAPI mutation hook. So hooks useAPI mutation and I'm gonna pass in the API from convex generated API dot board dot update like that from here, I'm gonna be able to destructure mutate impending and Then I'm gonna go inside of my onSubmit function.
In here, let's go ahead and let's properly mark this as form event from React. So import form event from React. And it's going to handle an HTML form element. And then in here, we're going to have an event. Did I do this correctly?
On submit is a form. Oh, it's not a form event. It's a form event handler. My apologies, form event handler. And then we can do event, prevent default here.
And then let's go ahead and call mutate and let's pass in the ID to be initial values.id and title is very simply gonna be the new title from the state controlled. And let's add .then here and let's call toast from sonar. So make sure you import this. I'm going to move it here with the global imports. So toast.success is going to be board renamed.
And since we programmatically opened this, we also have to programmatically close it. So we're going to call onClose here. And let's call .catch here, which will very simply just toast the error failed to rename board. Like that. So now I believe this should be it.
Make sure you have isOpenOnClose and initial values from user name model. Usually when I work with Sustent, I like to do this state state, but I think I don't have to do that. I think I confused the docs somewhere reading that I have to do that for some shallow updates or something I'm not sure if you know in the comments whether I have to do this it seems to work just fine without it I think that's just default right I guess oh yeah and we also have the pending here. So let's make sure we use this pending. So I'm going to use the pending to disable the input and to disable the save button.
So now if I try and rename this, there we go, It is immediately renamed. If I go ahead and change this. There we go. It is immediately renamed. Let's just confirm in our database that that is also working.
Where are my names? There we go. Title, rename, change this, untitled, untitled. So if I go ahead here and rename this one to 123, it's immediately updated right here. Let's go ahead and try the delete one more time.
That is working as well. Perfect. So we wrapped up all of the actions here. The copy is working, the rename is working, and our delete is working as well. Perfect.
What's left is to create the favorite functionality and then we can go ahead and implement the additional logic to query by favorites and to query by search. Great, great job.