In this chapter, we're going to go ahead and learn how to populate our playlists. In the previous chapter, we've learned how to make them, right? So if I go inside of my custom playlists here, I will be able to see my playlists. But how do we actually add items here? That's what we're going to focus on in this chapter, starting by implementing this video menu add to playlist model right here.
We're going to have to implement that component, and we're going to have to implement a special playlist procedure called get many for video. So the goal will be to implement a normal infinite load of all the playlists this user has but with a special video ID so we know so we can check whether a video is already a part of a playlist or not. So we are going to implement like a little toggle functionality. And we have to implement the add and remove procedures for a playlist. Let's go ahead and start by building this UI.
So I think that's an easy win we can get right away. We can start by searching for the video menu component inside of modules, video UI components, video menu. And let me remove the to do here, because that's what we are going to be building right now. So here it is add to playlist. Obviously this component will have to have some states.
So let's go ahead and just add a normal use state here. Open and set open. Let's be specific. Open playlist add model and set open playlist add model. Let's add this.
Let's import use state from React. Let me just move the constants here. There we go. And now let's go ahead and change the add to playlist to call setOpenPlaylistAddModel to true. And now we have to go ahead and implement the playlist ad model.
So what I'm going to do is I'm going to copy one of the existing ones we have, which can be instead of our playlists, actually, I think we have a model here we do playlist create model let's go ahead and copy and paste it and create playlist add model like this instead of here let's go and immediately rename the playlist create instances to playlist add. There we go. Now let's go ahead and import that component here. So what I'm going to do is I'm going to wrap this entire thing in a fragment and I will very simply render the playlist add model component. I will pass in an open to is actually open playlist add model and set open set open playlist add model.
Actually I prefer maybe is open playlist and is open like this. I just think that makes more sense. Set is open, is open and set is open. And this will be on open change actually. So now if you go ahead and pretty much in every single video that you have, you should be able to open create a playlist, which is not the one we want, but we are managing to open something from here.
This should be visible in all of our video rendering styles. If I go inside of my history I should be able to do that. If I go inside of my liked videos I should be able to do that. If I go ahead and search untitled and go to the search page I should be able to do that. If I go ahead and search untitled and go to the search page I should be able to do that.
Perfect! So just confirm that you have been reusing the correct video menu component throughout all of our cards. Now what we have to do is we have to modify this so it actually renders what we wanted to render. Inside of the playlist add model, let's go ahead and change the title here to be add to playlist. And inside of here what we can do is we can remove the form component entirely.
And instead, we can prepare a div here with flex flex column and a gap of two. We can remove the on submit method. We can remove the create method, the utils and the form. Let's go ahead and clear out all of these things, even though we might add them later, I just want to keep it clear for now and no form schema either. So just a very, very clear model here.
And now what I want to do is I want to go inside of my procedures, playlist server procedures, and we're going to implement getMany for video. We can save some time by copying the existing getMany method here and let me just move to the top and render the getMany for video. I want it to be descriptive because it is a very specific type of procedure here. It's going to be a protected procedure and instead of the input, besides the cursor and the limit, we're going to have a required video ID, which will be a type of string UUID. At least in my case, UUID.
Okay, and now that we have this, let's go ahead and do the following. So we're going to include everything from the playlist, video count. We don't really need the user, so no need to select that, right? We don't need it. But we will be needing, actually, I'm not entirely sure.
Maybe we will. Let's leave it. We can check later. But here's what I want to do here. I want to, so we are now loading all playlists basically, right?
It's exactly the same as the normal GetMoney. But this one will have a special field called Contains Video. So basically, inside of this data here, we're going to have a field called Contains Video, and it's gonna be a Boolean, true or false. What this is supposed to do now is supposed to go over our connecting table which we call playlist videos. So it's supposed to search through those videos and find a matching video ID for this specific playlist ID.
I have not been able to do this using normal query builders. I have only been able to do this using the SQL operator. So that's what we're gonna do here. I'm gonna check if the video ID has been passed. And if it has been passed, I will use the SQL operator, which you can import from the Drizzle ORM right here.
And we're now going to build the SQL query. You can go ahead and give this a type of Boolean because that's what it's gonna be. And you can also prepare the same thing here and you can just manually add false here like this. Oh it doesn't have video ID because we have to destructure it from the input. There we go.
So inside of here what we're going to do is we're going to open up parentheses and we're going to write SELECT exists SELECT 1 FROM playlistVideos.pv WHERE pv.playlist__id == playlists.id pv, where pv.playlist underscore id is equal to playlists id, and pv.video id is equal to video ID. Now, no matter how I tried, I could not reproduce this specific sub-query using the query builder. It could have been very much possible that I was too tired at a point when I was writing this. So perhaps you are seeing an obvious solution. If you are, feel free to go ahead and explore it.
Perhaps, you know, try writing this in a normal sub query which we, I think, used somewhere before. But I was not able to do it using a common table expression. That's for sure. That caused way too many problems. But in here, we're just doing a sub query inside of here.
So now when you hover over here, for each video ID that you pass, it will tell you whether that video is already a part of the playlist which we are currently loading or not. Let me just double check my cursor here. So we will still be loading only the users playlists and I think that the cursor stays exactly the same. Great! So now Let's go back inside of our playlist ad model.
Let me just see, what's the last thing I change? Okay, video ID. Make sure it's like this. Make sure there are no typos here because I think that this is not typed at all. So you can type whatever you want here.
So be careful. Playlist underscore ID, video underscore ID. And here's another thing. In order for this to work for you, you have to use the underscore here. Right?
Because the way we use camelCase here, it's just an object attribute annotation for JavaScript, which is the language we are using. But this name inside of each of our type is an actual attribute in our database. So if you, for whatever reason, used this accidentally, that's okay. It's going to work. It's fine.
But you will have to change it to match this exactly because what we're doing here is we're using pure SQL. So you have to know the name of your fields. So that's a very important tip for you. Make sure that the video ID and the playlist ID have proper naming conventions here. In case you decide you want to change yours, for example if you did this and now want to change it to this, no problem.
You can do it. I would recommend before you do that, leave it as it is, leave it like this, clean your entire database, right? Remove all items, then do the change. Because if you do the change and save and then open the studio it will probably break. So just keep the change as it is, delete all your videos and then after you modify this you have to do Bonnex Drizzle Kit Studio push and then just create new videos.
All right, I hope I cleared that up in case someone has some problems here, but I think this should be enough for us to focus on our playlist ad model. And now go ahead and simply fetch these things. So we're simply going to get our data from TRPC from the client and we're going to be querying this for the first time. So let's just add a TRPC playlist, get many for specific video, like this, use infinite query. In the first argument we're going to add a limit, the default limit, and we're going to pass in the video id.
And in the second one here we are going to pass the get next page param. Oh, and we are missing video ID. Okay, so let's just go ahead and add video ID here as a required string. There we go. And besides this, I don't want to...
So right now what's going to happen is that this useInfiniteQuery, which you might have noticed, is not useSuspenseInfiniteQuery. It's just useInfiniteQuery. We are not prefetching this anywhere, right? This will only exclusively come from this client side model. That's completely fine.
No need to prefetch this. So make sure you don't accidentally use the suspense one. What's happening now? Inside of my video menu, I'm immediately rendering this right here, which means that this will immediately fetch. That's not something we want.
I only want it to fetch once the model is actually opened, not when it's rendered. We're going to add enabled, and it's only going to be enabled under the circumstance that we have the video id prop and we received the open prop. Only then are we going to enable this query. And now let me go inside the video menu and check do I have video ID in the video menu. I do.
So let's just pass in the video ID. Video ID. There we go. Now let's focus on the playlist add model here and what we can do already is we can try and do JSON stringify Data. So now you should be seeing all of your playlists once you click Add a playlist in form of JSON here.
Let's start by adding a nice loading indicator here. So I'm going to add isLoading like this and we're going to check if isLoading. Go ahead and render a div with a class name flex-justify-center and padding of 4 and render a Loader2 icon from Lucid React and give it a class name of Size 5, animate spin and text muted foreground. Make sure you have imported the Loader2 icon here. So now if I go ahead and refresh and open for the first time, it will be loading and it should be loading actually every single time that this opens.
And I think that we can improve this in a specific way in here. On open change, let's hijack it. So I'm going to add const handle open change, like this. And I will add reset here. Actually, this might be invalidated.
Okay, this is what I'm going to do. I'm going to add utils from TRPC use utils here, like this and onOpenChange, first thing I'm going to do is call the playlists, get many for video, invalidate for this video ID. And then I'm going to call onOpenChange. And then let's use that here. And let's just see.
So this is accepting open, Boolean, so just pass that here. And since I already have a prop called open here, I'm going to call this new open, just so we differentiate between the two, so we avoid any hoisting happening here. So now every time, this is actually invalid, maybe the one I should be using is not cancel, I just want to Reset it completely. Let me pause my video. Oh, reset.
There we go. That's what I want. So now, every time I open, it's loading. And when I close and try opening again, I was hoping it will refetch every time. But looks like it's not.
Maybe because of this, it's not accepting anything. Let me try. I'm basically trying so that every time it opens, it refetches from the start. All right, I'm not gonna waste my time with that for now. If you want you can explore it but I want to focus on some other important things.
But we will need the utils from here so we can leave it here safely. And now let's go ahead and let's render our playlists in a nice way here. So instead of rendering this, we're going to check if we are not loading. And if we have playlists?pages, flat map, playlists is not defined, that's because we are calling this data right here. How about we remap this to playlists like this.
So let's go ahead and do playlists, flat map, get the individual page, return all items from that page, map over individual playlists and finally return a button component from Components UI button. Inside of here, I'm going to assign the key to be each playlist ID, and inside I'm going to very simply render the playlist name. So now when you open a playlist, there we go, you should be seeing buttons which represent all of our playlists. Now let's go ahead and add some attributes to this button right here. We're going to start by giving it a variant of Ghost.
After that, a class name, width, full, justify, start, px of 2. And we are going to hijack SVG elements inside by giving them a height of 5 and a width of 5 or very simply size of 5. So if I hover over, this works. There we go. Let's give it a size large like this.
And let's go ahead and see the improvement. There we go. This is how it looks like now. And now what we're gonna do is we're going to check if playlist contains video. We will render a square check icon from Lucid React like this with a class name mr of 2.
Otherwise, if it doesn't, we will render a similar icon, which will be just square icon, both from Lucid React. So just make sure you have added them here. So now, since none of our videos are in a playlist, they should all look like big check boxes here that you can click on. And this is exactly what we wanted to achieve. And now let's go ahead and add the infinite scroll right here below, but only if we are not loading the initial elements.
So add infinite scroll here. And now we have to extract as next page from here has next page is fetching next page and fetch next page. And now we just have to assign all of those props here to the infinite scroll. And since the nature of this is a model, I would prefer if a user should manually click to load more elements. There we go.
Rather than the user having an immediate auto-scroll. So if they can't find it in their most recent playlists, because we order by most recent, they're going to have to click load more here and then find the one they prefer. Great. So all of this seems to be working just fine now. What we have to do now is we have to implement the remove video and add video procedures.
So let's go ahead and start back inside of our playlist procedures and let me see do I have any mutation here. I have, I have the create mutation. So I'm going to copy the create mutation and I'm going to add it here at the top. I'm doing this inside of my playlist procedures and instead of calling it create, I'm going to call this add video. It will be a protected procedure and inside of the input object, it will accept a playlist ID.
It will accept a video ID. So basically, the two things necessary for our connecting table. Instead of destructuring the name from the input, we will destructure the playlist ID and the video ID from the input right here. Now what we're going to do is we're going to implement a toggling function here. Let me go ahead and remove this thing here.
We're going to start by checking if we already have a playlist. Actually, yes. Let me just confirm. Let's check if the playlist even exists. Right, so await database select from playlists where equals playlists id to our playlist id.
If that is not the case, we are going to throw new TRPC error with a code of not found, meaning that we cannot add anything to this playlist. And now we're going to check if the playlist we are attempting to add something to belongs to our user ID. So if that's not the case, make sure you add the opposite equalization here. We can again throw a new TRPC error with code forbidden. Now we could have handled this in a different way.
We could have simply confirmed the user ID instead of the where query using and. I think in most of my use cases, that's exactly what I did. But I just want to show you that you don't have to do that. If you want to, you can load the thing every time and then show a more specific error to the user, if that's something you care about. But I think we might be saved the query if we immediately break it in the where procedure, but I'm not sure if that's exactly how that works.
But yeah, basically two options, Either do the check here or go ahead and add and here and combine the user ID to be the user ID. And then you don't need this check at all, whichever one you prefer. Once you've confirmed the existing playlist exists, let's do the same thing for the video. And now the cool thing about the video is that it's not going to be required to be created from the user. So immediately remove that and thus you can also remove the end because we're just gonna have a simple query here.
Instead of existing playlist this will be existing video from our videos. Let's ensure that we have that imported here. Videos from database schema, we do. There we go. So in case there is no existing video, we can just throw the exact same error here.
And now finally, let's check if we have the existing playlist video. Using await database select rom playlist videos where and and let's add two equals here. The first one will check for the playlist ID. And the second one, let me just fix this, will search for the video ID. So we are now finally using both the video ID and the playlist ID from the input.
So we are now looking at, is this already inside of a playlist, right? If it is, then this is a conflict. This is not something that should exist. So if existing playlist video, throw new TRPC error here with a code of conflict, Like this. And finally, let's do created playlist video.
We await database insert into playlist videos with values of playlist ID and video ID. And let me just fix this. And returning and return the created playlist video there we go so now what I want to do is I want to immediately copy this entire thing and I want to implement the remove video method. So let's go ahead and add remove video here and it's going to work very similarly. We're just going to do some reverse logic here.
So we are going to check for the same thing. Does the playlist exist and does it belong to the user? If it does, if it does not, we are going to throw not found. Now we are doing the same check for the video, but we don't care if it... Oh, here's a mistake.
My existing video query will never work. Let's fix first in our add video procedure here. So existing playlist is fine, but existing video should be using video ID and video ID. My apologies, videos.id with the video ID. Let me just go down here to confirm that in here I use the proper one.
I do. And in here I use the proper one as well. Okay, this one is fixed. Let's go back up to our newly duplicated remove video. When we look for an existing video, instead of searching for playlists ID, we are looking at videos ID and comparing it to the video ID.
There we go. If you don't fix this, this will always throw an error. Great. So now we confirm that the video we are trying to add to our own playlist is existing. And now we're doing a check.
Does this already exist? Is there already an existing playlist video? But this time we are expecting it to exist Because this is a delete method. So we're going to add an exclamation point here. And instead of conflict, it's going to be not found.
Meaning that I have no idea from which playlist do you want me to remove this video because that's what this is doing. It's removing the video. And finally, instead of created playlist video, this will be deleted playlist video. So let's delete where, and now let's go ahead and add the proper query here and playlists videos playlist videos dot playlist ID matches the playlist ID and playlist video video ID matches the videoId. Like this.
And return this. Deleted playlist video. If we've done everything correctly, we should have fully working add and remove video from a specific playlist. Now we can revisit our playlist add model And let's go ahead and create the mutations. So just below this, I'm going to add const add video, and I'm gonna make it TRPC playlists, add video, use mutation.
On success here, I'm going to get the data. And what I'm going to do is I'm first going to add the toast from Sonar. Let me just import and move the toast here. Like this. And I will say video added to playlist.
Then I'm going to call utils and I'm going to re-invalidate all of my playlists. I don't have to pass anything inside because they are private anyway. This is only loading my own playlists. Then let's go ahead a bit more specific and let's invalidate GetManyForVideo. The reason we need to invalidate this one specifically is because when we refetch getManyForVideo, that's going to refetch this.
And more importantly, it will give us new information whether this playlist contains a video or not. So we can show in real time user information via a checklist like, hey, this exists or doesn't exist. And besides this one, I'm gonna add it to do. Refetch, actually, invalidate. Playlists get one.
This is something that currently doesn't exist. But if I don't add a comment, I don't think we will logically come across this. So that's why I want to do it. And let's do toast error here. Something went wrong.
There we go. That's our ad video. Let's copy and paste this to remove video. Ensure that you change this to remove video as well. I think I've forgotten to do this three times.
So for now we actually don't need the data. We will need the data for this. So you can remove it for now and hopefully we are going to remember that we will need it back then. And this will be video removed from playlist like this. There we go.
And now let's go and find the button and let's find on click here. We're going to check if playlist contains video. We're going to call remove video mutate and pass in the playlist ID to be playlist.id because we are iterating over a single playlist and pass in the video ID which we have from the prop. Else, you are going to call addVideo, like this. Now, add a disabled prop with either remove video is pending or add video is pending, like this.
And I'm pretty confident that there's nothing else that we need to do here. I'm gonna go ahead and refresh this and you might have noticed that one of my videos has disappeared. That's probably coming from Mux. I think Mux has triggered my webhook. There we go and deleted my video because I'm on a free tier.
So remember after 24 hours the videos are up. You can see this one has 5 hours left. While I can still do that, let me zoom in. I'm not sure if this is too zoomed out for you. So now it doesn't belong to any playlist.
And I can also prove that by going inside of Bonnex Drizzle Kit Studio. You should always double check with your studio because you never know what your UI might be doing incorrectly. So I have no records inside of my playlist videos. That proves that there are no connections between any video and a certain playlist. I will now select this one and there we go.
I got a check mark here and if I refresh this I'm expecting one record here. Perfect. Which is that video ID which I can confirm here called Untitled and a playlist with us. There we go And I should be able to toggle it out. There we go.
Video removed from the playlist. And if I refresh this, there we go. And I should also be able to add my video to multiple playlists if that's what I want. Right. So that's what we wanted to achieve.
We wanted to add the ability to add a single video to multiple different playlists. Right. So This is one playlist. This is the same video. Another playlist.
The same video. Third playlist. Exactly what we wanted to achieve. Perfect. So that is working.
And we can now see what playlist this video belongs to. And if we go inside of our all playlists here, we should be able to see the number of our videos inside of a playlist but looks like it's not working until I do a full refresh. So let me see why this is not working because inside of our playlist add model here we definitely invalidate playlists get many. So I'm gonna go inside of my Playlists page and confirm that I'm using Playlists. Get many and I will confirm that my get many method here is doing the video count.
So it's quite odd that this didn't work. So I'm going to remember as that's that's that's has one video, but if I go ahead and remove it from that one it should be invalidated now. So let me go here and there we go now it says one video. Maybe the invalidation was simply a little bit late But there is one thing that I think we can do in this chapter, simply because it makes sense that we do it here. So this is the last thing we will do.
And it's specifically concerned inside of this get many procedure. So this specific procedure, which is used inside of the playlists page.dsx, right? This page right here. As you can see, all of my playlists are telling me that while I do have one video inside of these two, it has the same thumbnail as the other ones indicating that it's empty. So how about we go ahead and actually create a query which will allow us to load the current thumbnail.
Now again there is only one way I've been able to do this because this connecting table seems to be specifically problematic to work with with query builders. I added a comment from my original source code where I explained why we have to use a SQL builder for this. It is because Drizzle lacks native max aggregation for subqueries. That's what we have to do here. You're going to see exactly how we will achieve the following.
Inside of our select for our getMany, inside of our playlist server procedures, we need to add a thumbnail URL for each of our playlists. And that thumbnail URL needs to come from the following. It needs to find the proper playlist videos. Then it needs to find the proper playlist and then it needs to find the video. And once it finds the video, let me just find my video.
Once we finally find the video, we have to obtain the latest one, thumbnail URL. So only the latest one that's been added. That's the issue. So I've only been able to do this like this. Adding a SQL operator and giving it a type of string or null because it's possible that we don't have a single video inside.
Double check that you have the SQL operator here. We have already used one for the contains video and now we will be using it here for the thumbnail URL inside of the getManyProtected procedure. So this is the query we are going to build. Open parenthesis and write, select v.thumbnail underscore URL from playlist videos as pv, like this join videos V on V ID equals playlist video dot video underscore ID where PV playlist underscore id equals playlists id, order by pvUpdated underscore at descending and limit by one. So that's the query we have to do.
Again, double check that your video underscore ID, thumbnail underscore URL, playlist underscore ID and updated underscore at are all written like that inside of these parentheses inside of the strings. That's very important. So you have to have a proper, you know, created underscore at. If you don't, that's fine. Just use the same field name here correctly.
So specifically we need the thumbnail URL to be written with an underscore. We need both the video ID and the playlist ID from our playlist videos to be written like this with an underscore. And we also need the updated ad to be the same. So now we should have a thumbnail URL of the most recent video we have added to a playlist. So what we can do is we can revisit our playlists playlist view.
We should have the playlist section component instead of modules, playlists, UI, components, sections, playlist, section. It should be right next to history and liked videos here. And now, in here, in the playlist grid card, instead of using thumbnail fallback, try adding data, thumbnail URL, or the thumbnail fallback. And now, if I refresh here, there we go. You can see the thumbnail of the last video I have added here.
Amazing. So what I'm going to do is I'm going to try and upload another video here and I will purposely give it some other thumbnail. So let me go ahead and just upload this video. Now I'm gonna be redirected here any second. Let me wait a few moments for this to be ready.
Subtitles are ready. This is ready. And I'm gonna go ahead and manually change my thumbnail here to a red one, so it's clearly visible. So let's just wait for upload thing to upload. There we go, that's here.
We don't even have to save, everything's already made up here. One thing I have to do is change the visibility of the video to public like this. Now we should see both of my videos and let me check. Instead of my playlists, my DFG DFGD has the blue one. If I go inside of here and choose to add to this playlist and now revisit my playlists, it has the newest video thumbnail.
I think this is so cool. I love when we build a query and it just works. I wish that I found a way to do it using the query builders. I might reach out to authors from Drizzle to see if they can help me with this because I think It's super interesting how one would do that. There's nothing wrong with doing this though, right?
I mean, if you know SQL, go ahead, use it. It's extremely powerful. I mean, you know that, I don't have to tell you that. But you know, I just love using an ORM to its maximum, Especially when it's this one, like Drizzle, which is basically like writing SQL, just using a query builder. Great, amazing, amazing job.
I'm super satisfied with this result. What we have to do next is we have to load the actual, we have to load the actual playlist individually when we click on it, which is gonna look just like this, right? As simple as that. And then we're going to utilize the on remove method from here so we can remove one from the playlist. But we are getting very, very close to the end.
Amazing, amazing job.