In this chapter, we're going to create the subscription list. We're going to add a few of our recent subscriptions in the sidebar itself, but we're also going to create a dedicated subscriptions screen, so that we can easily keep track of all the people we are subscribed to. We're going to start by building the get many procedure. So let's go ahead and revisit our source modules subscriptions procedures on the server, just to see what we have. We have the create, we have the remove, and I think that is it.
So let's go ahead and save some time by visiting, let's see, do we maybe have get many in the users? We don't, I think this is only get one. Let's use videos. We certainly have getMany in the videos. So find the getMany base procedure here and copy it entirely.
So we have the pagination and everything else. That will save us a lot of time. I will paste the entire getMany method here and it's going to be riddled with errors. We're going to resolve that now starting with changing the base procedure to protected procedure because logged out users can't have their subscription list here at all. We will not accept any category ID.
We will not accept any user ID either. Just a normal cursor with updated at as part of its configuration and a normal limit. So you can remove these items from here and since this is a protected procedure we have the context and inside of the context we can destructure ID, user ID from context user. Now it's up for us to fix this. So let's go ahead and import guest table columns because we will be needing it.
But instead of spreading the videos we will be spreading subscriptions. And we already have this imported because this is the subscriptions procedure. And we will simply load the user for each subscription because if you remember our subscriptions simply have a viewer ID and the creator ID. So we as a viewer will load all of our subscriptions and we will have to do an inner join on the creator ID to load the proper user so we can display the user information like this. So we are going to have the users so let's import them from the database schema here And we are not going to need the like count or the dislike count.
We will just need the subscriber count. So basically we want to learn what's the count of the subscribers that they have. Because we will be displaying it here. Perhaps it would actually be better to display that inside of here rather than the subscription element. So go ahead and do get table columns and passing the users here.
One quick tip for you. If you ever happen to go another level deep, like another level, and you try to do get table columns here and try users here, at one point you will start getting errors. Right? There's a limit to how deep you can go with the select here. I know because I tried to add like metrics and then I would add metrics inside, but eventually this starting to get some errors like another one.
And if you try subscriptions or something, basically at one point it will tell you that you went too deep, just in case you go exploring this yourself, But it will still work. It will just give you a TypeScript error. The Drizzle team has a pull request or an issue to resolve that. So yeah, it will definitely work, but you will get a TypeScript error. But for our case, we don't go that deep.
So we just need the subscriber count for each user. So make sure you spread the other user information and then do a sub query using database count subscriptions. And you're looking for the creator ID for the users ID object. And that is it for the select. So we are searching for from subscriptions here, and now let's interjoin users using subscriptions specifically on the creator ID.
And let's map that to user's ID, exactly like this. And now what we have to do is modify the this, we don't need this, we don't need the category, and we are always gonna have the user ID, so this will never be undefined and it's required. So we're now going to check, so that we only load subscriptions, where subscriptions viewer ID matches the currently logged in user ID. And then we're going to populate the creator ID from that model, because we don't have to populate ourselves, right? We are already logged in, we know our information.
So now let's go ahead and import or here from drizzle.org, larger than from drizzle.org and descending from drizzle.org. Make sure you have all of those here and now we're just going to fix our cursor here so instead of videos all of these will be subscriptions and instead of ID, because we don't have ID here, we will be working with creator ID. So let me just resolve this creator ID. And we're also going to modify the cursor here to not have the ID then, but to have creator ID instead. So now we should get an error here.
There we go. So updated at updated at and creator ID creator ID and creator ID last item creator ID but we'll leave updated at updated at as it was. Excellent. So that's it. That's our query here.
It's a protected procedure which exclusively loads the currently logged in users subscriptions and populates the creator alongside giving it a useful subscriber count. So now what we can do is we can go ahead and I want to do the sidebar first because it's easier to do. So let me just go into localhost 3000 here. Keep in mind that some of your videos might be removed by now. I think I had two videos, but now I have only one because it's been 24 hours.
So yeah, just keep that in mind. So now we're going to build the subscribers area here. So let's go ahead and revisit our modules, home, and we have components home sidebar. Besides personal section let's copy it and let's add subscriptions section like this. Subscriptions section.
Yeah, it's unfortunate naming here. I'm calling these three sections. I wish, I mean, maybe you can rename this to main items, personal items, and subscription items, simply because we call sections something else, right? We call sections places where we are going to use the suspend query most of the time. So maybe just change it to...
Actually, subscription is fine. Yes, subscription section, that is fine. Let's just check. Do we have subscriptions section anywhere else? We don't, but we will have after this.
So you're just going to be careful to not accidentally import from the home sidebar. Inside of the subscriptions section, let's go ahead and do the following. First of all we're not going to have any items. This will not be static, this will be entirely dynamic and let's rename this the subscriptions section. And we are not going to need the clerk and is signed in, but we will need data from TRPC, from the client, subscriptions, get many.
And we will do use infinite query here. And we are very simply going to limit by five and then let's go ahead and add the second argument here but the thing is we won't really need it because we're not gonna do pagination here. This will only be used to load just a few items and then we'll have a button which will lead us to a page where we can load all items because it doesn't make sense for me to add infinite loading in this part here. I kind of feel that's weird when we can just make a dedicated page. Same thing for playlists, right?
I'd rather we have a dedicated page for our playlist rather than having them listed all here. So I just added a few here, like history and liked videos because those are like the static ones but now we're going to create a subscription section and that's why I just want to load a few of those like five maybe or ten whatever you want. I will add the default limit here from my constants so I'm consistent. I will remove these and I will remove these as well. And now I'm gonna go ahead and try and render those.
So let's go ahead and instead of going over items, we are now going over data and we have to add data.pages here, .flatmap. And then I have to extract page and page items let me fix the typo here and then I can map over individual item which is the user and all the information we need. So this item will actually be a subscription, a single subscription. So the key will be subscription. We can use a combination, subscription.creatorID combined with subscription.viewerID because that's the actual unique primary key that we are using.
So the tooltip for this can be subscription.userName. That can be the tooltip when we are on collapsed mode. The path name will be active when we are on slash users and then on subscription user ID page of that user. OnClick does not need to exist here because what we will have is a link. And the link will simply go to this exact URL, so we can add that here.
FlexItemsGap4, that's fine. And now instead of using item.icon, we will be using a user author from components user author. Give it a size of extra small, image URL of subscription user image URL, and the name of Subscription user name. And inside of here, we are going to render that one more time like this. Subscription user name.
And let me just go ahead and confirm that we imported the user author right here. Now we just have to find a place to render this subscriptions. And let's also change this label from YouTube subscriptions. And now we have to go ahead and render this inside of our home sidebar. So let's go inside of home sidebar index and what I'm going to do is I'm going to import signed in from Clerk Next.js because it's a handy util which we can use like a JSX element and inside of signed in let's add a fragment so we can render both the separator component and the subscribed section.
Whoops, not this one. Subscribed, subscriptions section from .slash subscriptions section. So yes, we have to be careful. There are, because of the way we call them section, just make sure it's the one you just developed, the one you created here, this one. And now if you're logged in you should see your subscriptions here and if you go ahead and unsubscribe for example this should be refreshed so what we have to do is we have to find our use subscription method from subscription module hooks use subscription and there we go to do revalidate subscriptions get many utils subscriptions get many invalidate and you can remove the to do here and do the same thing for subscribe there we go so now if I refresh my subscriptions are empty but if I click subscribe here there we go The user appears in my sidebar immediately.
And you can see how it's active because we are on that user's ID. If I go somewhere else, it's no longer active. So now what I want to do is I want to add a loading indicator. And besides a loading indicator, I also want to add the button to go to all subscriptions page. Let's go ahead and just go inside of the subscriptions section.
And let's import the skeleton components so we can create a nice loader here from components UI skeleton. And what we can do here is export console loading skeleton, add a fragment here, and simply do 1, 2, 3, 4.map, get the individual item here, and render the sidebar menu item, give it a key of this. Let me just close this one. You are probably noticing that I used this method here. You can use this or you can use array from and then give it the length and then map, whatever you want to use.
I just remembered that we can do it this way as well. Let's add sidebar menu button here with the prop disabled. And let's add two skeletons here. Let's give the first one a size of six and a rounded pool as well as shrink zero. And the second one, a class name of height, bore and full width.
Fun fact, I'm pretty sure that sidebar element actually has their own skeletons. Sidebar menu skeleton and... Oh, it just has sidebar menu skeleton. Yes, but it's quite good, right? The issue is the sidebar menu skeleton, which I refused to use here, will generate the random widths for the items every single time, which is not the case for our subscriptions, which are pretty, you know, they're kind of look like buttons, right?
I just didn't like the way they had different widths every time. I feel more consistent with this. So let's go ahead and use the loading skeleton now. Since we're not using use suspense here, because we, inside of layouts, we cannot prefetch properly. So we have to use this.
I forgot to mention that yes we are not using use suspense infinite query because we did not prefetch the subscriptions anywhere so we can't use use suspense infinite query And if you try prefetching in the layout, it will just act as if you never prefetched. Basically, it won't work. So let's go ahead and do it this way because this is completely fine. And what I'm going to do is I'm going to check if we're loading. So for that I also need to extract isLoading like this and now we're going to check just above the data pages if isLoading.
Just go ahead and render the loading skeleton here and let's add instead of here if not isLoading and if we have data pages And let me just fix the fact that I never return anything here. So that should resolve the error below. There we go. And one more thing I want to do is right here after we end the pagination and let's do if not loading. So if it's loaded, add sidebar menu item here again, add sidebar menu button here, give it an as child property and is active of path name being identical to slash subscriptions.
Make sure you don't misspell subscriptions. I often add suscriptions, so just be careful with this. And inside of here, we're going to add a link element, which we already have, and give it an href of slash subscriptions. Basically it needs to match this one. And the class name, flex-items-center and gap of four.
Let's run the list items icon, actually list icon from lucid react with a class name size of four and let's add a span all subscriptions. And the class name, text small. So now you should see the text all subscriptions, but when you do a hard refresh, they are first going to load. You can see how, since this is not prefetched, it's a little bit slower than everything else but I think that's fine for a sidebar. It's not exactly some crucial information whereas this is important to be fast.
Quick reminder that if sometimes it takes a lot of time for you to load a specific page. Like you can see I pressed enter and I'm waiting for a few seconds for this to load. That's because it has to compile the search page first. As you can see it took a second to do that. The compiling will not happen in production.
The compiling is done during the build process, which is something that happens before we send it to production. So you don't have to worry about that part of being slow. Now that our subscription sidebar is done and it looks good in mobile mode as well, we can build the subscriptions page. Let's start by building this 404 not found page. So we're gonna go inside of our source app folder inside of home and we already have one subscriptions page which is inside of feed so we are now going to create an actual subscriptions page outside of feed directly in home Let's add page TSX here and let's go ahead and return the subscriptions.
And we should no longer be having a 404, it should now just say subscriptions here. And this should be highlighted here. So now what we are going to do is mark this as an asynchronous component and let's do avoid the RPC from the server and let's prefetch our getMany. Prefetch infinite to be more specific and let's add a default limit here from the constants so we don't have to worry about mismatching limits in our suspense later on. There we go.
Now we have to mark this as hydrate client from the server as well. And we now have to render the subscriptions view. Make sure you don't import anything from anywhere because this should be a completely new page. And inside of our module now for the subscriptions, instead of UI, we have to create a new folder called views. And what we can do is we can just copy from our playlists, UI, Views, just copy the history view because it's quite similar.
So go instead of subscriptions back and instead of the new empty views just paste it here. Now let's rename it to subscriptions view. Rename this to subscriptions. Let's change this to be all subscriptions and the text will be view and manage all your subscriptions. And no need for any, if you want to, you can, okay, let's do it.
Instead of history view, instead of playlist module, you can also find the history videos section. Go ahead and copy it, go back inside of subscriptions UI, create sections folder and paste history videos section. This will now be subscriptions section. And if it asks you to update any imports, you can press yes here. It should only update the imports from here, this newly created views.
So that's fine, it's not gonna mess up anything else. So for now, and be careful, Subscriptions section has the exact same file name as our Home components, Home sidebar Subscriptions section. That's why I said it's an unfortunate name. We should have called it Main Items, Personal Items and Subscriptions Items. So close the home module, close the playlists, and let's focus on subscriptions module, UI sections, subscriptions section.
Let's change all instances of history video to subscriptions like this. So subscriptions section suspense, same for skeleton, and same for the main export constant here. And now inside of here, instead of loading get history, we are going to load the subscriptions, but let's just import subscriptions section here. So now when you do a refresh here, just go back to the page, the new home subscriptions page and import the subscriptions view from the modules. And now you should have all subscriptions and an error which also loads the history.
The error is because we are mismatching what we prefetched here and what the Hydroid client suspends here, which is a completely different query. So let's go ahead and fix that by going and prefetching subscriptions. Get many like this and now we are going to have subscriptions here. Let's immediately prepare the utils from trpc.use.utils because we will be needing it. And let's also prepare our const unsubscribe method which we can copy from use subscription, the hook inside of the same subscriptions module.
So just copy unsubscribe method here like this and to simplify this page will never be accessible for non-authorized users, so no need for this part. No need for this, and just import toast, just in case something else goes wrong. This will never come from video ID so no need for this either. And instead of ID here, this is what we are going to do. We are going to get data when a new subscription has been created or removed, right?
So this is why I always tell you, even inside of our remove mutation here, we always did deleted subscription and we chose returning. So we can always get it back. So we knew, okay, this is what I just deleted. So from here, I can now extract data, create our ID. I can refresh that user's profile page so that when we visit it, it will no longer say that I am subscribed to them.
Right? That's why it's so useful that even when you delete things, you still return them back. And that's why the database will also give you something back when you delete that record. All right, so now we have the... And this will say unsubscribe.
We only need unsubscribe because this is a page which shows the people that we are already subscribed to. So when they unsubscribe, they will simply be removed from the list. All right, so that's everything we need from the new things here. And now let's go ahead and fix this iteration here. So this will be a little bit simpler actually.
It's not going to have a desktop and a mobile view. It will just have a single flex call and gap for view like this and you don't need this part here. So it's going to have a simpler skeleton as well. Instead of videos, it will be subscriptions. Instead of rendering a video card, we're going to be doing the following.
We will add a link from next link and give it an href of each individual user. So that and this is not a video in the end, this is a single subscription. So subscription user ID. And inside of here, let's also give this a key of subscription creator ID. Creators should never repeat.
And inside of here, we can just render JSON stringify subscription. Like this. JSON, stringify, subscription. Like this. So now when you refresh, you should see the JSON of the Antonio user.
And let me just see what's going on here. Subscriptions users, there we go. That was the issue. So now you should be able to click here and it should just redirect you to that user's page. Great, so now obviously what we have to do is we have to build this subscription item component which will in a nice way render this.
So let's start with adding a name to be subscription, username, image URL, subscription, image URL, My apologies, user, image URL. After that, subscriber count, subscription, user subscriber count. And on unsubscribe, we'll just call unsubscribe, mutate, and pass in the user ID to be subscription, user ID, or creator ID, I believe. Both should work just fine because they are the same thing. And it will be disabled if unsubscribe is pending.
So now let's go ahead and build the subscription item. So we're going to go inside of components and build subscription-item.tsx like this And let's start with building the interface, subscription item props, which has the name. Instead of avatar URL, it's going to be image URL because that's what we are using here. So image URL. And now let's export const subscription item here.
Let's go ahead and return a div. And let's assign these props right here. We're gonna structure the name, the image URL, subscriber count, on, unsubscribe and disabled. Let's start by giving this div a class name of flex-item-start-and-gap-of-4 and render user avatar from components user avatar here. Inside of here, we're gonna give it the size of large, image URL of image URL and name of name.
You can go inside of the subscription section and you can now import the subscription item. You can remove the video grid card, we are not going to need it. And you can remove video row card here as well. And let me move link here and let me just separate these for now. So we are still using these just for the skeleton but we will change it later.
So now you should start seeing some user information here. There we go. So the user avatar here matches this one. Perfect. Let's continue developing inside of our new subscription item component.
So besides the user author, we will have a div with a class name flex1. Inside another div, let me just indent this one, with a class name of flexItemsCenter and justifyBetween. And let's add a div which will have two elements inside one beneath another. One will render the name of the user and it's going to have a class name of text small, and then below that, a normal paragraph rendering the subscriber count, and give it a class name of text extra small and text muted foreground, and give this the number of subscribers. So now you should see the name of that user and you should see the number of subscribers.
And now what we're going to do is, outside of this div, render the subscription button from .slash subscription button and passing the size of small. OnClick will take an event, prevent default, and call on unsubscribe. Disable it if the entire component is marked as disabled, and the default to is subscribed. So usually our .slash subscription button, which is located inside of a modular subscriptions UI component subscription button, has always been used alongside a hook use subscription, right? So we had the proper is pending on click here, and then we would use that and the dynamic, are we subscribed or not?
But we don't have to really do that here because this will definitely load the subscribe user. We only need the on unsubscribe method here. Great, so I think that is it. I think that is the component here and you should be able to unsubscribe from here. And there we go, the user goes away.
So if you go ahead and find the user, let me just try and find their profile. Since the video has been removed, I just use the back button to find them again. And there we go, we have the subscriptions list here. So one thing that I want to show you is usually when I wanted to format the count, I used the entl thing, entl, every time I needed a number format notation compact. But I see that here in my source code I use to locale string, which might do the same thing that I wanted.
I mean, it's not really visible because the number here is one, but if it was like 1523, can I even do that? How do I test this? Okay, maybe not. This is a number. Okay, how about I do this?
I'm just trying something, const test 1536. And let's try test, look at string. Yeah, you can see that it formats it with a comma. So basically that's what I want. I want the number to look good.
What if it happens to be like a much larger number? Yeah, I think this is good. We can do to locale string. I mean, I always use the ENTL one simply because I don't know. Okay, we are using it here for compact.
So maybe that's why it makes sense here. But I think that to locale string accepts both the options for number format options. So yeah, maybe we could have done it easier with this. Yeah, basically, if this doesn't work for you, I mean you can just do this, you can do this, you can use our ENTL, whichever one you prefer. You can create a util which will help you be consistent.
I should have done that probably. But yeah, So now let's wrap it up by just adding the subscription's skeleton. So we need to build the subscription item skeleton. Let's go inside of the subscription item here. And let's export const subscription item skeleton, which will just be a mock of whatever we just built down there.
So a class name, flex item start and gap for skeleton from components UI skeleton with a class name of size 10 and rounded full representing the user avatar and then a div here with a class name of flex1. Another div with a class name of flex, item center and justify between. Now a div which will just run into skeletons one below another, which are going to represent the name of the user and the subscriber count. So let's just make it a little bit smaller like this. This is height four, this is height three, perfect.
No need for a third skeleton here, but we will be using the third skeleton here. And let's give it the class name of height eight and width of 20 representing the unsubscribe button. Make sure you've imported the components you have skeleton. And now let's go inside of our subscriptions section. And now we have to build the proper skeleton here and we can do that quite easily.
Just copy this, add it here, remove the outer div, remove the mobile one, there we go. And just simply render the subscription item skeleton. Make sure you import this and remove these two. There we go. If you want to render 18 of them or render eight of them, whatever you think looks better.
There we go. Amazing. You just wrapped up the subscriptions. I'm pretty confident this is the last thing, the last module we had to implement. So amazing job so far.
This is my biggest project so far and I'm extremely proud of how well it ended up being. And I'm sure you are for building it and getting this far. Good job. I will dedicate another chapter through going through my to-do here to see if I'm missing something here. But I think that most of this to-do's are just some suggestions.
Yeah, we already did this one in the seed script. No need for this. We've kind of confirmed, yeah, this might be not needed. So you can remove that. You can go inside of your, let me close everything else, just a second.
You can go inside of your source app home. Let me close this. Inside of your source app, home route group layout, and you can remove this. I think this does nothing for layouts. Nothing will change here.
But still, I will dedicate a chapter just going through my code once more, confirming with my final source code. Maybe I missed something obvious. Maybe you know that I've missed something I'll hopefully I will remember it if you know that I've missed something but I didn't add it feel free to leave a comment and see you in the next chapter And we also have to do the deployment. Yeah. Anyway, let me just mark this as done, done and done.
Amazing, amazing job.