So now let's go ahead and let's implement the favoriting functionalities. Right now this button does nothing, in fact it will redirect us to a 404 page. So the first thing I want to do is go inside of convex and go inside of schema right here. And in here we're going to extend our schema so that we also have a user favorites table. So let's go ahead and create a new table called user favorites.
And inside of here, we're going to use define table. And we're going to pass in the organization ID for the favorites to be v.string. We're gonna pass in the user ID, which favorited, which is also a v.string. And we're also gonna pass in the board ID, which is gonna be v.idboards because we have that relation right here above. And now let's go ahead and add a couple of indexes here.
So we're going to use a very simple by board index which is going to use the board ID field. Then we're going to go ahead and add a by user and organization. We're going to go ahead and add the user ID field and the organization ID field. Then we're going to go ahead and have an index of by user and by the board so let's add the user ID and the board ID and last one is going to be an index by user board organization so let's add user ID, board ID and organization ID. So we're gonna later check whether we need all of these indexes I might have gotten ahead of myself here.
So basically the way convex works is that it kind of allows you to do both SQL and NoSQL kind of schema. So you technically could have made, you know, favorite users and make this a V.array of V.string, which would represent something like an array of user ID, like that. The problem is that is not exactly scalable, right? And they themselves don't recommend doing that if you have more than a limited number of objects inside of an array. So the proper way to do that is how you would do it in a SQL relational database which is that you would create a connecting table between the user and the boards.
We're gonna call this user favorites, we could have also called it user boards but I think it just makes more sense because these are favorites. And inside we have all the necessary information we need to later check whether a board has been favorited or to load or favorite boards. Great! So now what I want to do is I want to go ahead inside of well first let's check if this is okay so go inside of the terminal where you are running npx convex dev and just confirm that once you save this it says preparing functions and then convex functions are ready and you can always go inside of here and let's check our schema again and now we have user favorites here. So if I show schema there we go we have user favorites right here.
Perfect. So now let's go ahead and let's create some functions to add something to the favorites. So I'm going to go inside of convex and I'm going to go inside of an individual board right here. So let's go ahead and do the following. Let's go ahead and write export const favorite to be a mutation, which will accept arguments, which are going to be an ID of the board we are trying to favorite, which is a type of VID boards.
Then it's going to have an organization ID, which is a type of VID, sorry, V dot string. And we're also going to have a handler function, which will have the context and the arguments. So first things first let's go ahead and let's check if we have the identity. So context sorry await context auth get user identity if we don't have identity we can immediately go ahead and break this function by throwing an error, unauthorized. Now let's go ahead and try and fetch the board we are trying to favor it.
So const board is gonna be await context.database. And we didn't have to use query here because we were only fetching one. So we can use get. And because we established that the ID is of the board's schema, we can just simply pass arguments.id and that is gonna load the board if it exists. So then if there is no board we can just simply throw a new error here, board not found.
So now that we have those two checks, let's extract the user ID so we don't have the right identity subject. So it's clearer for us. So this is the user ID. And now let's attempt to fetch an existing favorite. So we have to check whether this item which we are trying to favorite is already in the list of our favorite items.
So const existing favorite is gonna be await context.database. And let's go ahead and chain a query of user favorites. Let's use with index, by user and board organization. Let's get the query and I'm just going to collapse this in a new line. Query equals user ID to match user ID equals board ID to match board underscore ID and also organization ID to match arguments organization ID.
And let's just not add a comma here, all of these are chained events. And here's what we're gonna do, we're gonna fetch the first that, because it's supposed to be unique. Do we maybe have .unique? Yes, I think we can also use .unique and then this will be just a single element, right? So if you used collect for example then this will become an array as you can see.
But since we know we are only expecting a single element like this to exist, now you can see that this is just an object of a single element. So in the original source code which I wrote, I used .first but now I think it might be better to use .unique. We're gonna see if this is still working. So now let's write if there is an existing favorite. We're gonna throw new error here, board already favorited.
And then let's finally go ahead and do await context database insert inside of the user favorites. Let's go ahead and pass the user ID. Let's pass the board ID to be the fetched board underscore id and organization id to be arguments.organizationid. And lastly, we can return the board. Like that, or we can do, yeah, we can just return the board.
It doesn't really matter what we return because we are not going to be redirecting or anything but yeah let's just return the board for now. Alright so now we have the functionality to add something to favorite so now let's go ahead and let's create the opposite one to unfavorite so I'm going to copy and paste this entire thing here and just below that I'm gonna paste it and I'm gonna call it unfavorite so it's gonna work the same way first let's fetch the identity like that let's get the board let's get the user ID here and in here what we're gonna do is the opposite so we're gonna attempt to fetch an existing favorite but this time we're gonna do this if we are trying to unfavorite something that doesn't exist in our user favorites query So we have to use the opposite if clause here by adding an exclamation point. And then instead of this being the error message, the error message is going to be favorite in board not found, meaning that you cannot unfavorite something that you didn't favor in the first place. And then instead of insert, we're gonna use delete, then that's gonna be existing favorite underscore ID.
Like that, there we go. So now we have both favorite and unfavorite functionality here and for the unfavorite, we actually don't need to pass the organization id in here. So let me just confirm that arguments.organizationid. Yes, we can actually use the board organization id. So inside of the unfavorite, since we fetch the board, we know the organization ID for that board right here.
And also technically, since board can only exist in a specific organization ID, we actually might not need to use the this index. So we can make it even simpler by removing this and using by user board. Let's go ahead and check my schema. So we have by user board. Yeah.
So I don't think we need by user board ID, but when we are creating one, we do need it. So I'm going to add a little comment here. Check if org ID needed. So I think we don't need it. I think we're going to use organization ID to fetch something else.
But I'm just going to leave a little comment here in case we have a bug. This is the first place I'm going to look at. So what I did is inside of the unfavorite mutation I changed it to use a simpler index so I'm using by user board instead of a by user board and organization ID because I don't think I need to fetch by that because a board ID can only exist inside of a specific organization. So let's go ahead let's continue for now and let's just see perhaps we can also simplify the original existing favorite then I might think. We'll see.
Okay, let's leave it like this for now. And now what I want to do is revisit my app folder dashboard underscore components board card right here. And inside of here where we have our footer we have to create this on click functionality which is going to toggle between favoriting something and unfavoriting something. So first things first I want to go ahead and I want to import my use API mutation from hooks use API mutation and then I want to import API from convex generated API like that. So let's go ahead and let's extract our options here.
So useAPI mutation, API board favorite and let's copy and paste this one for unfavorite. So we're gonna have three of those, and let's go ahead and destructure both of those. So the first one is gonna have a mutate, which we're gonna change on to be an alias on favorite and it's gonna have pending state, which we're gonna rename to pending favorite. This one is also gonna have a mutate of an unfavorite and a pending state of pending unfavorite. Like that.
And then what we can do is create const toggle favorite. If is favorite. We're going to go ahead and call on unfavorite and passing the ID. Else, we're gonna go ahead and call on favorite and passing the ID and organization ID. Like that.
So let's go ahead and use this toggle favorite here. And let's just paste it inside of here. I think that should be enough. And then for the disabled, we can use if pending favorite or if pending unfavorite like that. And now here's the thing.
So we are hard coding the is favorite. So right now, whenever I click on this, it should give me the create action, right? So here's what I'm gonna do. We don't need a success message for this because it's gonna be immediately shown to the user that something has been favorited later when we implement the actual logic to load that, but I do wanna get the errors. So what I'm gonna do is add .catch toast from sonar error failed to unfavorite and I'm also going to go ahead and add a catch for unfavorite here So we know if something goes wrong.
So fail to favorite. And make sure you import the toast from Sonar itself. And let's try it out now. So now I believe that my user favorites is completely empty here but if I click on one of this right here this redirected me and user favorites has been successfully created you can see I have my user ID I have my organization ID and I have my board ID which is linked to my actual board right here with the title rename. Perfect!
So this works definitely and now what I want to do is go inside of I believe that if I try it again I'm going to get an error but let's go ahead quickly inside of the footer right here and I think that we can improve this on click right here let's do the following, Let's create our own const handle click to get an event, which is a type of react.mouseEvent, HTML button element, and mouseEvent. So let me just see if I imported any of those. No. All right. So HTML, nothing.
Okay. All of this should just be existing in the scope. And what I'm going to do is event stop propagation and event prevent default. And then I'm gonna call onClick. And then use this handle click instead of direct onClick here when we favorite something.
So now when I click on the star icon, I should not be redirected, but there we go. I got an error, failed to favorite. Why? Because this is already in my database of user favorites. So I cannot favorite again something that has already been favorited.
But if I click on this, let's just wait for this to hide. If I click on another one, then I have no errors here and I have a new record inside of my database. But if I try again, I get an error because that one is already favorited. So what I want to do now is I want to modify my main board, my main convex boards function right here so that in the end I also include a field is favorited for this logged in user. Also if this is not working for you here's what I recommend that you do.
So inside of app, where were we? We were in the board card index. So if you want to, you can use the alternative. You can use const handle favorite and use mutation from convex react and pass in API board favorite. And then when you call handle favorite here, you're gonna have, Oh, you can see that you have some type errors, right?
So you would have to write this as ID as type of ID boards like that, for example. So if you're having any errors, it could be due to invalid typings. So you can always check that here. Yeah, so perhaps we are losing a lot with my custom use API mutation, but I really like the fact that we have pending here. So yeah, you can always fall back to using use mutation from convex react if you're confused, and then you can check your types more thoroughly.
Great! So let's go ahead now and let's go inside of the boards here and what I want to do is I want to query them so that all of them have a favorite relation. So after I load my boards, what I'm gonna do is I'm gonna write const boards with favorite relation and that's gonna be boards.map. We're gonna get the individual board and then I'm going to open a function and I'm going to return context.database.query user favorites with index by user board let's get the query I'm going to send this to new line and I'm going to call query.equals userId to be identity.subject and .equals boardId to be the current board that we are iterating over ID. And let's go ahead and get unique and then if we have a favorite let's go ahead and return an object of this exact board but also is favorite which is simply going to be a boolean based on the fact that whether we can load the user favorite or not, like that.
Great, so after we've added that, then what we can do is write const boards with favorite boolean is gonna be promise.all, and we can pass in boards with favorite relation inside. Like that. So let me see if I can expand this. Basically in one line. And make sure you return the new boards with favorite boolean right here.
So now if you go back inside of board list component right here, I don't know why it's collapsed, the data now should have the is favorite Boolean. You can see that my data has is favorite Boolean in here. So what I can do is I can change this from is favorite false to where we are either reading over our board cards in use board and add is favorite like that. And let's see if that is working. There we go.
So now if I click again I unfavorited it. If I click here I unfavorited it again and you can see how I'm no longer getting any errors because now our function knows when it should favorite something and when it should unfavorite something. Great, great job! So you've wrapped that up. And one more time, you know, if you're having trouble with this, I would highly recommend that you go ahead and use the native use query from convex slash react.
I thought this would be maybe a fun exercise but I can see how with this any typings some of you might be losing you know the idea of what actually needs to be passed there. But yeah, you can see that obviously this is still working but if you're getting confused you can go ahead and write it alternatively. So I can do it just for this for a little exercise. I'm gonna do that now. You can speed up the part if you don't want to but this is how you would write that.
So I'm gonna call const handle favorite to be use mutation API board favorite and then const handle unfavorite is gonna be use mutation API board unfavorite. I'm getting the use mutation from convex slash react And then in here I would very simply call handleUnfavorite and in here I will do handle favorite. And then in here I'm gonna go ahead and map this as ID which we can import I believe. Let me just check if we can import it. I should be able to import this.
ID boards. Oh my apologies. So ID as the type ID, which you can import from add slash convex underscore generated. So let's go back here. There we go.
So import ID from add slash convex underscore generated data model. And then when you're using that ID specifically, you can specify even further that this is supposed to be an ID of boards. But the thing is, even though TypeScript throws an error, you can see that this as doesn't change what we actually send here. So that's why our solution with use API mutation also works just fine, right? So Let's try it out and this, if this will work just fine.
There we go. You can see how this is working, but you can also see that my state is not disabled while I'm doing that because I have to now manually write isPending here. So I'm just gonna revert it back to the old one, but I just wanted to show you an alternative way if you're having issues with this or if you simply want to practice you know TypeScript with convex here. All right I think I have all the imports I need here. I think all of this is working for me.
Let me just confirm one more time so I can wrap up this chapter. Yes, nicely. I can add to favorite and I can remove from favorites. Perfect. What we're going to do next is wrap this entire thing up by enabling the search and querying by actually favorited items.