In this chapter, our goal is to build an individual playlist page. On this page, we will be able to see the videos that we have added into a playlist, and we will also be able to delete a playlist, as you can see with this little button here. Alongside that, this drop-down of each of our videos will give us an ability to remove a video from a playlist. So we're gonna start by building the Get Videos procedure so we can load custom playlists videos. We're then going to build the custom playlists page, which are all going to be very similar to our history and our liked videos.
Let's start by focusing on the get videos procedure. So I'm going to go ahead and go inside of our modules, playlists, server procedures. So this is where I want to go. Modules, playlists, server procedures. And what I'm going to do is I'm going to copy my get history procedure.
So either copy this one or the get liked one. One of those. Now I'm going to change this and rename it to get videos like this. Or you can maybe call it get custom if you want to whatever it makes more sense to you and this will need to have a playlist id which will be a required uuid string here so we can now destructure it here a required playlist id And instead of having viewer video views, we're gonna have a commentable expression called videos from playlist. Let's call this playlist videos like this.
And we're going to select the video ID from Playlist Videos schema. From Playlist Videos. Let me just fix the typo here. And in here we're going to query by playlist videos, playlist ID, matching the playlist ID, which we will enter through here. So that's our common table expression, which we can then join right here, like that.
Now, what we are going to do is we are going to remove the viewed at property because we are not going to need one. And while we are here let's also immediately go and fix our cursor so we don't forget to do that. The cursor will have a normal updated at field right here. So make sure you change this immediately. Now let's go ahead and see whether we have to do something inside of this data here.
We are probably going to have to do a left join on the videos from playlists. There we go. So let's do an inner join from videos from playlist. And inside of here, we're going to add videos from playlist.videoID. So basically, the videos from playlists are already being queried by the current playlist ID for which we are loading the videos and now we are just ensuring here that we load that specific ID and we join that playlist.
So we have that connected record here. So make sure you have added this. And now what we have to do is we have to modify our cursor here. So the cursor will be even simpler because we are working with the videos records. So change all three instances here of videos.
And instead of a view that, change all five instances to use updated at. As simple as that. And this should completely resolve our cursor. And now change the next cursor to be updated at as well. So all of this should be back to updated at, which is basically our normal way of using a cursor, right?
When we used get liked and get history, we had the special ways, but now we don't. And for the user ID here, well, we probably should find a way to utilize the user ID here as well. We are joining. Oh, okay. We are doing that here.
Yes. So we don't have to do any specific query here because we will ensure that, oh, actually, no, no, No, no, no, no, no, no. So basically what I wanna make sure is that the playlist that we're trying to load only belongs to this user who is currently logged in. Or if you want to, you can make this a public playlist. So here are our choices.
For example, I'm going to comment this out and I'm gonna remove the context from here. What this will then do is, What would make sense to do now is to change the get videos from protected procedure to base procedure. So you have to import that. This will essentially make all playlists public to look at. Private to add to, but public to look at.
So if you want to, what you can do is you can go ahead and make this public with base procedure, or you can add protected procedure here if that's what you prefer. And then what I would suggest you do is extract the user ID and do the following. Existing playlist await database select from playlists where and and in here you would check the following if playlists ID matches our playlists ID and if our playlists user ID matches the user ID only then continue with the query meaning that if this does not exist, existing playlist, throw new trpc error, code not found. So whichever one you prefer. For example, this will make viewing all playlists private to the user that created the playlist.
And you have to add context here. So pick one. Either make it a base procedure or make it a protected procedure and simply do this check here if you don't want anyone else to have access to this playlist. Great, so I think that's it for our Get Videos procedure. Now we have to go ahead and build the individual custom playlist ID page.
So let's go ahead and go inside of our source app folder, home, playlists. And now I'm just going to create a dynamic playlist ID here. Like this. Make sure you don't misspell this. Use proper camel case here and add a page TSX inside.
And I will for example copy the content from my history here and just paste it here. So export default is important and we will be prefetching the get videos here. Get videos like this. Let's create an interface here for the page props like this and let's assign them here, page props. We should have the params here, and you should be able to extract the playlist ID from await params.
And then you will be able to pass the playlist ID here. And you should have no more errors. And let me just go ahead and add this here because it seems like we didn't have it instead of my other page. So make sure that you add export const dynamic force dynamic to the new page which we created and visit your history and just make sure that you add it here as well and visit your liked and make sure to add it here as well. And let me just visit the normal playlist page and let me add it here as well.
Export const dynamic, force dynamic. Like this. So all of those pages, basically every single one of our pages, except the out pages will need this because we are doing some prefetching in all of these. So let me check my search page has them, my videos, video ID page has them. I think that every other page that I do has them except the playlist ones.
All right. I'm staying. I'm getting off course. Let's stay back instead of playlists, playlist ID page here, now that we are prefetching the videos for a specific playlist, let's create videos view. Make sure you don't accidentally import this from anywhere and add playlist ID here, prop to be playlist ID like this and remove the history view.
And we are now going to create the videos view. Now, the videos view can easily be created by copying the history view. Let's go inside of Playlist UI, Views and copy the history view and rename it entirely to videos view. Now, inside of videos view, let's go ahead and modify this for now to just be custom playlist and this to be custom playlist as well and rename this to videos view. Now go back to the page and import the VideosView component from Modules, Playlists, UI, Views, VideoView.
It's missing this prop, so we have to go back inside of the VideosView and we have to create an interface here, videos, view prop, and extract the playlist ID here as well. Great! So now if you refresh, I'm now on a custom playlist, right? So if I go ahead and click on my all playlists here and click on a random playlist, I should see custom playlist and I should get an error unauthorized because our prefetches are incorrect right now. So now clicking on these should not lead you to any errors.
You can double check this by going inside of playlist grid card and check that the link leads to slash playlists and then the ID of a playlist because that is the new route which we've just created. So now I want to build this header component first. This is what I'm gonna do. I'm gonna go inside of the videos view and I'm gonna replace this entire thing with a component playlist header section and pass in. Playlist, I.D.
Playlist I.D. Like this. Let's go ahead and let's build the playlist header section here. So I'm going to go inside of my sections and I'm going to add playlist header section dot TSX like this. And the first thing I'm going to do is I'm going to add an interface and then I'm going to export on playlist, Playlist header section like this.
I'm going to assign the props. I will destructure the playlist ID and I'm going to return a suspense instead of this div from react error boundary from react error boundary and then playlist header section suspense and I will simply pass the playlist ID here to be playlist ID basically the thing that we always do and let's go ahead and add a full back here and I will just add a paragraph here change this one to loading for now and let's build the playlist header section suspense. We don't need to export it but it needs to be a constant. Let's go ahead and assign the props here. Playlist ID and now we are gonna go ahead and build something here.
Let's just ensure that we did this correctly so that we can go inside of the videos view and import the playlist header section from the dot dot sections playlist header section. So if I refresh we should see a text which says build something here. And what we are going to put inside is the following. So you can go inside of history view and copy this div with you know history, videos you've watched, all of those things and you can put it here like this and give the outer div a class name of flex justify between and item center and now instead of here we're going to say videos from the playlist. And we're going to have a button component from components UI button right next to here like this.
And Let's go ahead and give this a variant of outline, a size of icon, a class name of rounded full, and let's add a trash to icon from Lucid React. Let me just move it up there. And now you should see the text history, videos from playlist, and you should have a trash icon here. What we have to do now is we have to get one playlist. Basically, we want to load the name of the playlist, right?
So in order to do that, we actually have to build one more procedure here, which is going to be a little bit simpler. So I'm going to go back to my modules, my playlists, and my procedures here. So I did get videos and now I'm going to do get one protected procedure or public procedure, whatever you decided. Let's add the input id string uuid And let's add the query to be an asynchronous query. Let's extract the ID from the input, which we can get from here.
Input and context. So this is the input. And from context.user, we can destructure the ID and remap it to user ID for clarity. And now let's get the existing playlist to be await database select from playlists where and and then let's add first of all playlists ID matches the ID from our input and the second one user ID matches the user ID. If there is no existing playlist, we can throw new the RPC error here with the code of not found.
Otherwise, we can return back the existing playlist. There we go. And while we are here, I want to build one more procedure called RemoveProtectedProcedureMutation. And let's also just add an input here. The input will be exactly the same.
There we go, without the query. Asynchronous mutation with input and context here. And you can destructure the ID from the input, you can destructure the ID, user id from context user like this and then just go ahead and do the same thing which we just did here find the playlist and throw the error if it doesn't exist like that and then it's time to delete the playlist. So deleted playlist, await database, delete playlists, where and And you can just copy these two. So that's the playlist we want to remove.
And add returning here. And return the deleted playlist. And actually you don't even have to do this check here. You can just do this instead. No need for that upper one, because this should always, since we are using returning, it should always tell us which one we removed.
If we get null it means that it was not able to remove anything which means that this query failed. So either the ID was incorrect or we don't own that ID which we are trying to delete. There we go. So I think that's it for our procedures. We can now head back inside of the playlist ID page first.
So go inside of home playlists playlist ID page and besides the videos we are now also going to prefetch playlist get one and just normal prefetch with an ID of playlist ID here. There we go. Make sure you're doing the prefetch here. And now in the videos view in the playlist header section, we can go ahead and do the following. We can get the playlist from TRPC which we get from the client .playlists get one use suspense query and pass in the ID to be playlist ID.
There we go. Let's go ahead and add remove here to be TRPC playlists, remove use mutation on success. Add a toast from Sonar. Make sure you've added the import here. Toast.success, playlist removed or deleted, whichever one you prefer.
Add the utils so we can do some TRPC invalidation. And let's add utils. Whoops, my apologies. Utils.playlists.getMany.invalidate. Dot playlists getMany invalidate.
And let's do router, which we have to add here. Use router from next navigation. Always be careful to not accidentally import from next router because that's incorrect and push to slash playlist so we can go back to our page where we can create a new playlist if needed and in here something went wrong there we go And now let's go ahead and replace this with our playlist.name because we have prefetched it and we have used SuspenseQuery which will be loaded right here. And inside of this button here let's add on click here which will call remove mutate and pass in the ID playlist ID and disable the button if remove is pending there we go so now that we have this let's wrap it up by adding a very simple skeleton here. Const playlist header section skeleton will just be a div with two skeletons inside.
Skeleton from components UI skeleton. Let's give this a class name of let's flex column and gap Y of two. And let's go ahead and give both of this a class name of height 6 and width of 24, change the bottom one to 32 and the height of 4 and use the skeleton in here. And make sure you have imported the skeleton right here. And of course, make sure you actually return this here.
I forgot to do that. There we go. And now it should be much nicer when I refresh here. Oh, we have to mark playlist header section into use client. So when I refresh here, there we go.
You can see how I have a nice loading indicator first. Excellent. Now let's go ahead and let's fix the lower part which is currently loading the history videos section and let's replace it with videos section. Do not import this from anywhere, simply pass in the playlist id, playlist id like this and let me remove this And now we're going to create the videos section, which lucky for us is going to be identical to some other modules. So inside of playlists, UI sections, copy the history videos section and rename them to videos section.tsx.
I'm gonna go ahead and find all instances of history and remove them. So it's just the videos section, videos section skeleton and suspense and change the playlist to be get videos here. And we now also need to incorporate this, the props, and let's add them here. Props, and let's spread them here. And let's make sure we use them here.
And simply pass playlist ID alongside get videos here. There we go. So we should now be correctly prefetching everything here. Nothing should be changed and we should be able to import our new videos section from dot dot sections video section here. And now if I refresh, there should be no more errors and we should be seeing only the videos which are in a playlist.
In here, I have one video, so I only load one video. In here, I have two videos, so I load two of them. And if I click on a random one, it's empty and I should be able to delete it now. There we go. I am now also able to delete my playlists.
Perfect. One more thing we have to do is enable on remove method here. What I recommend we do is go inside of Playlists add model component and find... Oh there we go we can also do this to do so definitely go inside of Playlists add model here and do utils Playlists get one invalidate and now we and do utils.playlists.get1.invalidate and now we need the data from here and passing the ID of the playlist to be data.playlistID There we go. That's the one we need.
So delete that. And also you should delete, you should invalidate get videos for this playlist ID. And you can remove the to-do. So copy these two and make sure that you also do the same thing for the remove video in place of this comment here. And now that our...
I'll also add the data here. And now that our remove video is working fine, we can go inside of the videos view, inside of videos section, copy that and add it here. So you should now have remove video here. Now, I'm just unsure if we can actually invalidate get many for video. I think we can if we use data.
Yes we can using data video ID. So you can do that here. Import those from Sonar and we can simply add the utils here. The RPC use utils. There we go.
And now simply pass this copied remove video to our video row or grid card so I'm gonna do the same thing to both of those add on remove method which will call remove video dot mutate and pass in the playlist ID and video ID to be video.id. There we go. Let me go ahead and collapse this simply so it's more visible for you. So the same thing goes here and here. And all of these should now work just fine.
There we go. Make sure you've added on remove and now when you hover you should be able to remove an individual video from a playlist. I'm just unsure if this broke or if it just didn't revalidate so let me check my remove video here. It uses the playlist ID And inside of here I passed the video ID from the current iteration, so it should definitely work I believe. I'm gonna do a refresh here and see what happened and maybe try again.
Okay, looks like something is going on. Something is going on. Oh, maybe instead of our video roll card, we never assigned on remove. Let me check that. Maybe that's the issue.
Let's see. We are passing it to the video menu. Are we using it in here? Looks like we're not using it in here. We have to add it here on remove.
And I think that this will automatically fix both my mobile and my desktop view. So let me try removing. There we go. And let me try removing this one from mobile simply because it uses a different kind of card. There we go.
Perfect. So we've wrapped that part up completely. Everything works just fine here. I can remove the videos and now all of my playlists are immediately refreshed. Excellent.
We can delete the playlist and we can safely from any part of our project, yeah, I think one place we did not test was from here, but we should also be able to do add to playlist here and it should work just fine. If I go to my playlist, there we go. We can find it here. Everything is revalidated now. Excellent!
So what we ought to do next is we have to build the user's profile and we are pretty much done. One thing that I think we can do here simply because I don't want to waste time later is this views comments and likes. Let's just fix this quickly. We can do this quite easily because we have all the data we need. Let's go inside of the app folder, studio, studio, page.tsx, go inside of studio view and go inside of videos section so inside of UI my apologies module studio UI sections videos section And now simply find the place where we render the views.
And inside of here, render video.viewCount. Looks like it's missing, so we're going to have to add it. But go ahead and render video.commentCount here. And go ahead and render video.likeCount here. Since none of these exist, that means that we have to fix our studio getMany.
So you can go inside of Studio Module, Server Procedures and find GetMany right here. And this will be quite easy to fix. We are going to focus here on the Select method. So first of all, let's go ahead and do GetTableColumns here and pass in the video. Videos, so import get table columns from drizzle ORM.
Then add the user. Since we are doing inner join, I believe. Oh, it looks like we're not doing any inner join here. So that's our next step then. After from videos, make sure you do an inner join on the users.
So we are only, yes, this was a big mistake actually. In our studio, we only have to load our videos, right? Not other users videos. Yeah, it's a good thing that I checked this because I would have completely forgotten about this. Okay, now we are technically restricting that here.
Yes, that's fine. But it's good to have an inner join as well here. Okay, and now besides this, let's go ahead and let's add our view count. So import video views from the schema. Now let's go ahead and let's add our comment count.
Import comments from the schema. And let's go ahead and add our like count. Import video reactions from the schema. You should have video reactions, videos, video views, users and comments here. And now if you refresh you should see the proper amount of information here.
So if I go and add a comment and a like, let's see if that will be updated. One like here and one comment. Let's go ahead and leave the comment here. There we go. If I go instead of edit video content here and do a refresh, one, one, one.
Great. Amazing, amazing job. I'm of course, before we end the project, I will go through out my entire code to see if we are forgetting some things like this, like this inner join, even though this inner join would, without this inner join, nothing dangerous would happen. We are still only seeing our videos thanks to this, but it's still good to do an inner join on the users. Great, Amazing, amazing job.
Let's go ahead and mark these things as complete. We've created this, this, this, and this. Amazing, amazing job.