In this chapter, our goal is to create the individual user page. We're going to achieve that by first adding a banner URL and banner key to the user schema. And we are going to use that in this field right here. So this is going to be a banner. It's going to be optional and later we are going to implement an upload thing, uploader for this, but by default it's just going to be a gray area.
But I suggest that we create these two fields simply so we have the proper type script so we can at least load the placeholder even if we don't really have anything. And then we are going to have to create the users get one procedure which will be responsible for loading the info about the user and instead of the get one procedure we are also going to load user subscriptions count and user videos account And if we are watching our own page, we will see a text, go to studio, otherwise it will be the subscribe button. And that means that instead of the get one procedure, we're also going to connect the viewer subscription. So we can tell the user, hey, you are already subscribed to this user or no, you are not subscribed to this user. And then we're going to modify our videos get many procedures so that it accepts user ID prop and then we can reuse the whole procedure simply to load a feed of videos for that user.
And we are going to do all of that in the user ID page. So let's start by adding banner URL and banner key to the user schema. So I'm going to go ahead and do the following. I'm gonna go inside of my source database schema.ts and I will find my users here and I have a to do to add the banner fields. Let's add the banner URL which will be an optional text with banner underscore URL and I will have the banner key banner underscore key.
That's it. Now let's go ahead and immediately push that. So Bonnex drizzle kit push and that should also resolve this little issue that we have here which says the bannery URL does not exist. That's gonna be fixed now once we give it a proper push to the database. There we go.
And now let's go ahead and let's create the users get one procedure. I don't even know if we have the users procedure. I can't really remember. Let's go inside of modules. Do we have users at all?
We do have users, but we have no procedures here. So what I suggest we do is we create a server and procedures.ts inside. Let's speed things up by loading procedures from videos and just pasting them here. Now, we are going to have much, much simpler procedures here. So we can remove GetManySubscribed, of course.
We can remove GetManyTrending. We can remove GetMany. And we're just going to focus on the GET1. This is the one I'm interested in. So I'm going to find the end of GET1 and I will then remove everything else.
So I just want to speed up the process of adding just a single procedure here. Let's rename this to users router like this. And now let's go inside of our DRPC routers underscore app. And let's add users here, users router. Let me just add users and let me move these to the top.
There we go, now we have the users router. So now what we want to do is load the user. Here's how we are going to do that. So we're going to be using the base procedure, the id will stay the same, we are going to have clerk user id, so optionally we are going to load the database user or not. This is perfect, We are going to reuse this so this can stay the same.
But our viewer reactions will not be needed here. But our viewer subscriptions is perfect. We will be needing that. Basically what we are going to do here is we are going to prepare a temporary table from which we are going to load all subscriptions for the currently logged in user if such user even exists. And then we are going to join that inside of our query here.
First of all, this will not be existing video, it will be existing user. And we are not working with the viewer reactions anymore, so we can remove that and just leave the viewer subscriptions here. Now For the get table columns, we will no longer be spreading videos, but instead users, because we are working with the users schema now, which means that we can remove the individual user here. Let's go ahead and remove everything else just so we have a clearer example here. And let's go ahead and do the following.
Let's add viewer subscribed. So basically we will be telling the user who loads this page if they are subscribed to this user or not. So in order to do that let's add is not null and let's pass in viewer subscriptions dot viewer ID. Map with boolean. So now you were subscribed will be true or false.
Now we have to ensure that we are properly joining viewer subscriptions. So we are using this table which loads all of the subscriptions from the currently logged in user and we are querying it so that it only checks for the current user ID that we are trying to load as the creator ID. So it should only return one result which we can query by is not null here. I'm going to remove this group by here because I think we don't need it. And we can remove the left join for viewer reactions completely.
And now let's go ahead and replace the from videos here to be from users. And we actually don't need the inner join for the users because we are querying the said users. And the where query will be users id matches the input id like this if there is no existing user we will throw not found otherwise we will return the existing user like this so now that we have this I want to add a few more things here, starting with the video count, which will be database count the videos, equals videos user ID where users ID, like this. And let's add the subscriber count, database, count, subscriptions, and then equals subscriptions, creator ID matches the user ID. My apologies, users ID.
There we go. So now we can tell the user if they are subscribed to this user, how many videos they have and how many subscribers they have. Basically you are searching for subscriptions where the current user we are searching for is the creator. Great! And we have a left join here which means that this query should work just fine.
I believe that this should be it. And now, let's go ahead and clean up everything we don't need here. The workflow, the mux, the UDAPI, and the sending. All of these things are completely unneeded here. And let's also remove these two.
There we go. Now that we have the user's router, let's go ahead and let's create the users page. So I'm gonna go ahead inside of source app folder home and I will create users and then I'm gonna go ahead and not create a new file but create user ID here and then page TSX like this. Inside of here let's go ahead and create an individual page, and let's return a div user page. Now, what should happen is that we should be able to go and click on a user and we should be redirected to this user page right here.
You should no longer be seeing 404s. You can of course confirm that by visiting video grid card for example and find video info. Inside of video info confirm that your user avatar is properly wrapped in the proper slash users and then user ID and same thing for the user info here. Basically confirm that you have no typos inside of this redirections and confirm that you have you have properly created the name here users slash user ID inside of the home route group. Now let's continue focusing on the page here.
First things first, we are going to need the user ID prop from here. So let's create an interface for our params. Let's mark this as an asynchronous component. Let's destructure the params and let's add page props here. Let's go ahead and destructure the user ID from await params.
And now let's prefetch using void trpc from server dot users get one, prefetch and pass in the id user id. There we go! We just prefetched the user information from here. Now let's add our hydrate client from the RPC server and inside of here we're going to render the user view component and pass in the user ID to be user ID. Just like that.
Now let's go ahead and build the user ID. So we're going to do that inside of the user module here. Let's just find users. We have UI here. Let me just close videos.
We have UI, but let's create views here and let's inside create userview.tsx. Let's create an interface page props or maybe user view props which will accept the user ID and export const user view. And let's return a div user ID. Now let's go ahead and add these here and extract user ID. Now let's import the user view from here and we should be able to see the ID now.
There we go. I can see the ID of that user and I can see the ID of this user. Perfect. Let's continue developing inside of the user view. I'm gonna start by giving this outer div a class name of flex, flex column, and a maximum width of 1300.
Let's go ahead and add a px of 4, pt of 2.5, mx of auto, margin bottom of 10, and gap y 6. Now let's create the user section which will accept the user id. Make sure the prop name is user ID as well. Let's go ahead and develop the user section by creating a new sections folder instead of users UI and add user section.tsx. The user section prop will accept user id and let's export const user section here props user section props and as always we are going to return suspense from react error boundary from react error boundary and then user section suspense and simply spread the props here let's go ahead and add pullback for both of this change this one to loading and let's go ahead and build the user section suspense component.
The user section suspense component will accept the same props but we can destructure them so it's easier to work with. And inside of here what we are going to do is we're going to fetch our user using trpc from the client. So make sure you have added the proper import. Make sure you don't mix up server and client imports. Let's go ahead and do trpc.users.get1.useSuspenseQuery and pass in ID of userID.
And inside of here you can return a div jsonStringify and we should now be able to see more information about the user. But we just have to import the user section from ./.sections, user.section inside of our user view right here. So this is the component we've just added. So now if I do a hard refresh here, it seems like I am getting an error. Let's see what exactly seems to be the issue here.
Can't read the properties of undefined reading get one. I believe that's because our user section should be marked as use client. Let's see if we are still encountering some errors so we can fix them. Looks like we are not. I can successfully load my user.
I can see the banner URL, the banner key, I can see the clerk ID, the total video count, the total subscriber count, and I can even see the fact that I am subscribed to this user, which is true. When I click on subscribe here, this user's video should load, and it does, which means that I am subscribed to this user. Excellent. Now let's go ahead and build the user page banner component. So I'm going to go ahead and give this div a class name of flex and flex column.
And then I'm going to add a user page banner component and I'm going to pass in user entirely here. Let's go ahead and develop the UserPageBanner, which we can put inside of the components here. UserPageBanner.dsx. Let's go ahead and before we continue this, create the proper types. I'm going to borrow the types from videos so we save some time.
I will copy the entire file from my video module's types and I will add the same to users. And instead of having video get one output, we are going to have user get one output, which will look for users. And we can remove this one. Now let's go ahead back inside of the user page banner here and what we are going to do is we are going to create an interface user page banner props which is going to use our new types right here. Let's export consuser page banner.
Let's go ahead and assign user page banner props and let's extract the user. Let's go ahead and return a div with a class name, relative and group. Now let's go ahead and let's create a very simple gray area which will represent an empty banner. Let me just fix the typo here, class name. Make sure you import CN from LiveUtilis because this will be dynamic depending on whether the user has uploaded a banner or not.
It's going to have full width, a maximum height of 200 pixels, height of 15vh, medium on height 25vh, background gradient to R, from gray 100 to gray 200 and rounded extra large like this and also add background gray 100. Actually, let's do this. Add user check for banner URL. If it exists, we are going to use bg cover and BG center. Otherwise, use BG gray 100.
Perfect. And we can also add style here because we can't do this with CSS. If we have background image, we are going to use it. So check for user banner URL. If we have it, go ahead and inside of template literals write in URL user banner URL and then undefined otherwise.
Now let's go ahead and check the following. We need to add the user ID, the current user ID from use out from clerk next JS. So let me just move that to the top. And we're going to check if the current user that we are looking at, its clerk ID is exactly the same as my currently logged in user's clerk ID. In that case, that means we can edit this.
So let's give this a type of button. Make sure you import button from components UI button. Let me just move it here. And what we're going to do is give it a size of icon and the class name, absolute, top four, right four, rounded, full, background black with 50% opacity, on hover background black with 50% opacity, basically we are overriding any effects on the hover, opacity 100% by default. On larger devices, opacity will start by being invisible.
And then when we do a group hover, which is this, the entire content is the group. So when we hover, we are going to change the opacity to 100. And let's add transition opacity and duration 300. And inside, add it to icon from Lucid React with a class name of size 4 and text white. Make sure you have imported this.
Now let's go to the user section and let's add the user page banner. Rom components user page banner. And now when you do a hard refresh here you should see a nice banner here. Since I'm on another user's page, I don't see the button. But if I go ahead and go to my own page, now I can see the button here when I hover.
Excellent! So that is working just fine. Now what I want to do is I just want to export the skeleton from the user banner because it's going to be very very simple. So let's just prepare the skeleton from components UI skeleton and let's export const user page banner skeleton and return a skeleton component with a class name of pool width maximum height of 200 pixels height of 15 VH and a medium height of 25 VH. There we go.
That is the user page banner skeleton component. And that's it for now. We do have to add to do add upload banner model. We're going to do that later. For now we want to focus on adding the videos to the user section here.
So that's one part of user section semi-complete. Now let's go ahead and let's add the user page info component instead of the user section suspense. And this component will also accept the user user value. The user page info component will be created in the same place as the user page banner. So let's add user page info dot dsx.
We're going to start by having the same props by adding the types and let's export const user page info. Now let's go ahead and let's assign the props here. We can destructure the user. And inside of here, let's go ahead and return a class name, starting with py6. Before we move any further, let's import the user page info from the components.
Let me just see user page info like this. Make sure you have this imported. So you should know, well, you shouldn't really see anything, but keep an eye here because this is where it's going to be developed now. So we are first going to work on the mobile layout. So perhaps it would be better if you collapse this into mobile mode, so you can see what we are working with.
Let me try zooming in a little bit here. There we go. Let's go ahead and do the following. I'm gonna add a div with a class name, flex, flex-col, and an MD hidden. Inside, I will open a new div with a class name flex items center and gap 3.
I will then render a user avatar component from components user avatar. Inside of the user avatar component I will give it a size of large. I will give it an avatar, my apologies, image URL of user image URL And I will give it a name of username and a class name, height 60 pixels and width of 60 pixels. So now you should see the user author on mobile mode. On desktop, it should not be visible.
Now let's go ahead and let's add onclick here. And the onclick will do an interesting thing. We need to add clerk from use clerk from clerk nextjs. So what we're gonna do is we're gonna check if we are the owner of this account so we can do that again by adding user ID from use out from clerk No need to import from clerk react you can import from next.js It should work just fine. So let's go ahead and do the following.
If user.clerkId is equal to the user ID we just extracted from the clerk hook. In that case, trigger clerk, open user profile, like this. So now if you are on mobile and you click on your image here, it will open the clerk update model so that you can update your profile and add a new image here. Right, That's what we are basically allowing the user to do. They can click on their image and it will do the same thing as if they clicked on manage account here.
Now let's go ahead and continue with the development. So outside of the user avatar, we're going to add a div here with a class name flex1 and the minimum width of 0 and then we're going to add an h1 element user name let's give the heading element a class name text extra large and font bold Below that let's add a div with a class name, AllFlex, Items-Center, Gap-1, Text-Extra-Small, Text-Muted-Foreground and Margin-Top-of-1. Let's add a span element, user subscriber count, subscribers. And let's add video count, videos. And in between those two, you can add the little bullet item.
Again, I still don't know how exactly does one render this. Maybe I can ask AI. Wait a second. Okay. I think that we can use at bool.
Let's see. Yes, you can use at Bull. It should work just fine. Let me try zooming in. I can't zoom in.
I have to use this. There we go. You can use this at Bull and that seems to be working just fine. Great, so we now have the videos here. And now what I want to do is I want to create the optional button either to subscribe to the user or to go to the studio.
So go outside of this div, outside of this div, and outside of this div, and check if userId is equal to userClerkId, that means we are the ones watching this, so the button which we need, which you are going to import from components UI button, will simply give us a link from next link and go to studio text and an href to go to slash studio. Let's give this a variant of secondary as child prop and the class name of pool with margin top of three and rounded pool. Make sure you have imported the link from next link. So if you're looking at your own account, you should see go to studio button and when you click on it you should be redirected to the studio. Now, let me go back to where I was and now let's go ahead and instead of using this let's use the ternary operator.
So the alternative will be the subscription button. You can import the subscription button from Modulus Subscriptions UI Components subscription button. The same one which we have already used inside of the Video Owner component previously. In order to use this, you might already know, we need our Use Subscription hook. So let's make sure that we use this.
Let's go back inside of the User Page Info here, and I will add that here. We need the use subscription, so import it from modules, subscriptions, hooks, use subscription. We don't have the video ID and the purpose of from video ID is not something we're going to use on this page, but the rest is something we need. Now let's go ahead and use that inside of this subscription button here. I'm going to go ahead and give it a disabled prop of is pending or if not is loaded.
This is loaded part will come from use out. Make sure you have this. Is subscribed will simply mirror the user, viewer subscribed and on click will be on click. And class name will be full width and margin top of three. And make sure that this on click is the one you have exported from use subscription here.
There we go. So right now, since I'm looking at my own accounts, I can't really see much, but if I go to another user's profile here, I should see unsubscribe which right now doesn't really work. Here's why. If we go inside of our use subscription, it works. What it doesn't do is this.
It doesn't re-invalidate. Let's add utils, videos, my apologies, users, get one, invalidate. And instead of here, we are going to invalidate the user ID that we just subscribed to. ID, user ID. And copy this to the unsubscribe method as well.
There we go. So no need for users get one now. I'm gonna remove that from here as well but still leave this comment. I mean you don't need the comment I have them it's fine. So you can see that it works right.
If I subscribe, it will refetch the number of subscribers and the status here. There we go. So it's working just fine. And we've proven that our subscriber count is working as well here from the user section. Basically, in this get one method, everything seems to be working exactly as we expected.
Perfect. Let's continue building the user page info component. So we just wrapped up the mobile view, I believe. And now let's go outside of this div and let's build the desktop layout. So right now, if you expand, you won't see anything.
That's because the desktop layout is quite different than the mobile one. So I just found it easier to build in two different sections instead. We can start by adding the opposite of this, right? So let's go ahead inside, let's close this and we're just gonna do the opposite. So it's going to be hidden by default but on MD we will get flex and we are not gonna have flex column but we will have items start and gap four.
And then we can add the user avatar component here. And let me just go ahead and copy all of these attributes because all of these attributes are exactly the same so we don't have to spend time with those. But we will change the size to extra large and you can remove, actually keep the class name. Here's what we're gonna do. We're gonna add CN here which you can import from libutils.
Basically on desktop we can be a little more interactive because we have the hover elements. So if user id is equal to clerk, my apologies, user clerk id, in that case cursor will be pointer, hover will be opacity 80, transition will be opacity, and duration will be 300. So now on desktop, since this is another user's profile, nothing happens. But if I go ahead and click on my own profile you will see that I have a clear indicator that something will happen once I click here and it does happen right so this check and this check match each other great So now let's go ahead and let's copy this part and we'll see how much we have to modify it. So basically the flex one minimum with zero and all the way including the user's name, subscribers count, videos, all of those things.
And let's put that directly below the user avatar here. Like this. Instead of text Excel, it's going to be text for Excel. And this will be flex item center gap one. Text will be small.
Text will be muted foreground and margin top will be 3 in this case. Like this. And then let's go ahead and let's copy this part right here. And let's render that just below this div for the flex one. Actually, no, we're going to render it inside of here, like this.
So on mobile we render it outside of it, but on desktop we'll render inside of it. And we're going to modify the classes a little bit. It's not going to have a width of full, but it will be rounded full. And same thing for the subscription button, it will just have a margin top of three. There we go.
This is how it's supposed to look like. So this is on desktop and this is on mobile. There we go. And now if you go ahead and click on another user here you should see the subscribe or the unsubscribe button and it should re-invalidate immediately and tell you the user subscriber count. Great!
And if you are on your own account you should be able to go to studio here. Excellent! Now let's go ahead and let's do the following. We now have to load the videos for this user. So that is this.
We have to modify videos get many procedures to accept the user ID prop. Let's go ahead and do that by going inside of the videos module. There we go. Videos, server, procedures and find the get many procedure. And we are now going to add an optional user ID right here.
And what we are going to do with it is the following. We're going to extract the user ID here. And then we're gonna do the same thing we do with the category. If we have user ID, then videos user ID will have to match that user ID. Otherwise it will be undefined.
As simple as that. And I think that's it. We don't have to modify anything else. Also, if you don't like nullish or if for whatever reason it's causing you errors, you can also use optional. But I think that nullish works just fine and we use it precursor.
So yeah, let's use it like this. Now let's go ahead and do the following. Let's go instead of source app, home users, user ID page, and alongside prefetching just the video, the user info, let's do void trpc videos get many, prefetch infinite, user ID, user ID and limit of default limit from constants. There we go. We now have prefetched all the videos that this user has.
Let's go inside of user view and let's create videos section, user ID, user ID, like this. Let's go ahead and make this a little bit easier for us by copying the one from the home feed. Go inside of modules here, home, and we're gonna have sections, and there we go, home videos section. Let's copy the entire thing here because we will be working with this exact kind of grid, but just slightly modified, so not really exact. Let's go inside of the users UI sections and let's add videos section .tsx and let's paste the entire thing here.
I will now go ahead and replace the home instances and just remove them because we are going to call it just videos section here. We can remove the prompts and make it a required user ID in this case. So the suspense key does not need to be changed like this because the entire page will be different if the user ID changes. So no need for the key here. And now this is what we are going to do.
First, we are going to call this user ID. So this actually stays the same. This is perfect. TRPC videos get many user ID is exactly the thing that we are prefetching in here. Videos get many with user ID, prefetch infinite, EU suspense infinite videos get many user ID with the same limit.
Perfect. And now we're just going to slightly modify the grid here, but you can already, if you want to, go inside of user view and import the videos section from dot dot sections video section and you should be able to see the videos for that user. There we go and if I go to this one it's the blue one If I go to this one it's the red one. But I don't like how they look like. You can see that this is too small for the size which we have limited here.
So Let's go ahead and modify the grid just a bit. Go inside of the videos section here and we're going to start with this right here. So let's see, grid calls one, SM grid calls two, LG grid calls three, two Excel grid calls three, two Excel grid calls four, and then this one will have grid calls four as well. And this one will have grid calls four as well. And now, they will be just a little bit, take a little bit more space.
So you can copy this entire thing here and you can add it to the skeleton here so the skeleton matches. So when you refresh, one thing we're missing is the skeleton from above so that's kind of messing up the thing. Okay this works well enough and since this is actually four maybe we don't even need this entire thing. Yeah I don't think we need this at all because it's just gonna remember the last one we put there. So yeah, I don't think you need this if we are just overwriting it back to four and this one is four.
So I think this will be just fine. Let's build the proper suspense for our user section here because we just put the loading text and it's going to be easy because we have created all the necessary skeletons. So let's const userSectionSkeleton And let's return a div with a class name, flex and flex column, user page banner skeleton, and user page info skeleton. Looks like we don't have the user page info skeleton, but we do have the user page banner skeleton. Okay, so before we can add it here we have to develop the user page info skeleton.
Let's go inside of user page info and now we have to develop the user page info skeleton. So let's go ahead and start developing right here. We're going to start by having the div. And then we're going to have mobile layout first so the mobile layout will be hidden like this we're then going to open the first row of elements inside which will be flex, item center and gap 3 We are now going to render the skeleton component from components UI skeletons so make sure you import this and this skeleton will have the following attributes height 60 width 60 and rounded pool. Now below that we're going to represent the subscriber count and the view count like this.
And then we're just going to pretend to add the... We're going to pretend to add the subscribe button which will be with the pull here. So this is the user page info skeleton. So let's go ahead and import what we have already just so we can try it out. User page info skeleton and now use user section skeleton inside of this loading here.
And let me go ahead and try on mobile here the refresh. A hard refresh. There we go. You can see how it almost identically mimics what we are seeing, but on desktop, not yet. So now let's go ahead back inside of the user section skeleton, user page info skeleton, and we've finished the mobile layout.
Now let's go ahead and do the desktop layout. So this ends the mobile layout, this starts the desktop layout. It's gonna have a similar starting div here with hidden on default and the will be flex. Then we're gonna have a skeleton representing the avatar. And after that, we will have three items in a row representing the views count, the subscribers, and the subscribe button inside of flex1 minimum width of 0.
And that's it. That's our component. There we go. Now this is looking much, much better. You can now go ahead and visit the user page.
Perfect! Amazing! So one more thing that I want to do here before we go ahead and build the next part which is changing the banner. I think we're missing a little separator here. Let's go ahead and do the following.
Let's go inside of a user section here And let's add a separator from components UI separator. Let's see. User section suspense, user page banner. Oh, oh, my apologies. This is where I want to put the separator.
There we go. Right here. So put the separator in the user section suspense right here and then put it here as well. And make sure you have imported it. There we go.
Now it looks better and you can see how this loads in their own time. I really like how that works. Great! So now if you go to Studio for example, if you click on your profile here, you will get to redirected to slash users slash current, which doesn't really work. Now, here comes the question inside of our studio layout, Why, inside of my studio sidebar, inside of my, where is it?
My studio sidebar header, why do I link to slash users slash current? Why not just get the user ID from use user because that is the clerk ID. And if you remember our page.psx in the user ID expects the database ID, but there is a little trick that we can do. Let's do the following. Let's go instead of source, app folder, home, users, and alongside user ID, create current.
And this will not be a page, it's gonna be an API endpoint. More so in fact, it's gonna be a GET endpoint. So export const GET, make it an asynchronous, extract user ID from await out from clerk-nextjs-server. If there is no user ID, we can just return redirect from next navigation to slash sign in and now I'd simply attempt to find an existing user from await database from the slash database select from users from database schema where equals from drizzle orm users clerk id matches our user id from await out If there is no existing user still, go ahead and repeat the redirect here. Otherwise, return redirect to slash users and then simply use the existing user ID.
There we go. So a little hijacking here of our client side router by adding the backend. So now if you try this again, go to your studio and try clicking on your user and you will be redirected to your own account and it will be cool if we added that here as well so user can visit their own account through this little drop down here we can do that extremely fast Let's go inside of our out button component, which comes from modules out UI components out button. And there we go to do add user profile menu button. And what you can do is you can copy the user button link and add it above the studio and rename this one to my profile and go to slash user slash current and simply import the user icon from Lucid React.
That's it. And now, oh, and change the label if it is, okay. Let me do a hard refresh. And now we have my profile here and I can click here and I will get redirected to my profile. Amazing, amazing job.
You just developed the user page. What we have to do in the next chapter is implement a very simple banner upload. Let's go ahead and mark these things as completed. We have added the banner URL. We have added get one.
We have added these get many, and we created the user ID page. Amazing, amazing job. 34 chapters already. Amazing.