In this chapter, our goal is to start creating the video page. The video page will consist of three sections. The video section, where we are going to load the actual player, user or author information, subscription features, reaction features, and the drop-down, which will allow you to add things to a playlist. It will also include the views feature and a nice description expansion box. That's what we're going to focus on today.
By today I mean in this chapter. And we will do what we can at the moment. So for example, we will be able to load the author and we're going to have to mock the reactions because we don't have that yet. Same thing for the actual number of subscriptions and views. But we will populate what we can.
Then the other things which are a part of the video page are suggestions and the comments section. We don't really have the API for either of those at the moment, so we will simply create a placeholder for them. So this is the goal. Start by creating the get one procedure for the videos. And Inside of here, we're going to inner join the user, which will have the author information.
We're going to do our usual prefetching process, and then we're going to go ahead and do video section as much as we can. And for the other sections, just a placeholder. So let's go ahead and let's start. Let's start by developing the get one procedure. So inside of our modules, videos, server, procedures, we're gonna go ahead and develop get one.
Let me just confirm that we don't already have GetOne. We don't have GetOne in here. We have GetOne inside of a studio, but I've purposely separated those because in this GetOne, I want it to be a protected procedure, right? But for this one in the videos, I'm okay with this being a public procedure. I'm okay with anyone requesting to load that video.
So let's do this. Get one, public procedure or base procedure in our case. Let's go ahead and add an input. Id string uuid. And let's add a query here.
It's going to be an asynchronous function. And in here, we can destructure the input. We can also destructure, we can destructure the context as well, right? But there's simply no requirement for a clerk user ID to be here, right? We don't expect, we won't always expect that to be true.
So now let's go ahead and let's do the following. Let's attempt to find the existing video by using await database select from videos where equals and then pass in videos ID input dot id like this and simply return the existing video in here we can add if there is no existing video throw TRPC error with a code not found. Like this. So I've just imported the TRPC error here. Let's see, can I indent this?
I bet I can. Great, so you can choose how you wanna name this video, existing video. I think existing video is better just in case we import videos, which is already similar enough so we don't accidentally misplace our constants, right? Great. So this is quite simple, right?
This should be able to just load the video. And as you can see now, when I hover over the existing video, this is what I see. ID, title, string, description, max, blah, blah, blah, all of those things. But what if I also want to load information about the author? Because right here, I'm supposed to load the author's image, name, number of subscribers, and then later on also these things, right?
So how do we do that? Well, in SQL and at the same time in Drizzle, we do that using joins. So in order to understand which join we want, there can be left join, there can be right join, and there can be inner join. So in our case, what I do is I hover over inner join and in here you can see a nice explanation. Calling this method retrieves rows that have corresponding entries in both joined tables.
Rows without matching entries in either table are excluded, resulting in a table that includes only matching pairs. This is exactly what we need because we need to fetch the equivalent author for each video and if this doesn't exist in that case we don't want anything to be returned. We just fall back to not found because something's obviously wrong if a video doesn't have an author. That should never happen. So let's go ahead and pass in the users and we are going to load the user which matches from our video's user ID to user's ID like this.
And now that we have, let's add inner join before where like this and now that we have the user what we can do is we can modify the select So what you can do here is you could, you know, just pass in title to be videos.title and then do the same thing. Thankfully, we don't have to do that. Instead, what you can do is you can spread the schema, but you can't do it like this. This is an error. You do it by using get table columns from Drizzle ORM.
So make sure you have imported the get table columns here and simply pass in the videos schema. And then everything's okay. And now you can go ahead and add the user here and do the same thing for the user, like this. And now, if you hover over existing video, You should see that alongside all the other things, I also now have the user. And I can see all of this information, image URL, name, clerk ID.
So our real database user is combined inside of our record. And the cool thing is that we did all of that in one SQL query. So we didn't do two queries, one to load the video, one to load the user and then join them using JavaScript. No, we did it using SQL. So this will be much more performant than if we were to do it in a hacky way, right?
Great, so we have this, and this is basically the method that we're gonna do later on to add subscriptions and all of those things. But in order to do that, we're gonna go even more advanced by using common table expressions, which are something like a sub query, but a bit more performant. Great, Let's keep it like this for now. I think this is all we can do from here so we don't really need this context. There's nothing we can use it for at the moment.
So let's go ahead and let's start working on our page. I'm gonna go ahead inside of app folder, home and inside of here I'm gonna create videos and then I'm gonna create dynamic video ID like this and let's go and add page.tsx inside. So this will be a normal page here with a div video ID. And now if We go here in any of our videos and we click here in the link, we should no longer be getting an error. We should get redirected to this video ID page.
In case you're still getting 404, make sure that you didn't do any typos here and inside of your form section for the full URL you should have http://localhost3000.com slash videos video id so just make sure there are no typos and now you should be brought to the video ID page. So now let's go ahead and let's do our prefetching process. So we're going to start by marking this as an asynchronous component. We're also going to create an interface, page props, with params, promise, video ID. Make sure that this matches exactly the capitalization you put inside of the square brackets here.
And then go ahead and add page props, get the params, and let's just extract the video ID from await params. Like this. And now what we have to do here is void trpc from the server and prefetch videos get one dot prefetch passing the ID video ID. Double check that this looks exactly as mine remember you can also do this and it will not throw an error but this is not prefetching, right? This is prefetching, this is not make sure you prefetch Great, and now let's add hydrate client here from TRPC server.
And what we ought to do now is add a video view. Keep in mind that we are not going to import this video view. This is from the studio. That's a different one. I'm okay with calling them the same name, because it gets very long if you have to type in Studio Video View and Video Video View, right?
It just loses its purpose that they are smaller components, right? They're going to be in their own module, and it's fine if they have the same name in some other module because we are going to be careful about where we keep them. Inside of this UI for the videos go ahead and create a new views folder and then inside videoview.dsx. What we have to do here is we have to create an interface. We can call this video view props or page props, however you want it.
And then just export const video view. Let's go ahead and let's assign the props here. No need to destructure anything here because we already, I mean, no need to await anything here because we already did that in the actual server component. This is also a server component, of course, but this is the one from the app router here. And we're going to import the video view from videos UI views.
So make sure you do it from videos, not from any other place. And pass in the video ID, video ID. There we go. And now let's go ahead and actually render some UI here. So we're going to render flex, flex column, maximum width of a thousand 1700 or 1700 if my English numbers serve me right, MX auto, padding of 2.5, PX of four and margin bottom of 10.
And inside of here, let me add a paragraph video. So now let me just go ahead and try and zoom out and you can see at one point we are going to stop expanding so that's what this is doing it's limiting how much of a wide screen we are going to support. Great! Now let's go ahead and let's add a new div with the class name flex, flex column on extra large devices it's going to be flex row and then a gap of six. Now inside of here open a div with a class name flex1 and the minimum width of zero And inside of here add a video section.
Videos, just not videos, just a single video section. We don't have this yet so don't import it from anywhere. Now let's go ahead and create the sections folder here inside of videos UI and create the video section.dsx. The video section will have its own props, video section props. Let's export const video section like this and let's do const video section suspense as we always do because this will be a client component.
So in here we're going to leverage the prefetching that we did in the server component. So let's go ahead and just do this. First, return suspense from React, error boundary from React, error boundary, video section suspense. Fallback here will be a very simple error. Fallback here will be loading.
Now let's go ahead and assign the props here. Let's give it the video ID. And let's go ahead and assign the props here as well and pass it video ID. Passing the video ID, video ID. And now let's go ahead and let's return something here.
What we are going to return is going to be the data from our TRPC, which we are going to import from the client, because this is a client component. Trpc.videos.get1 useSuspenseQuery and pass in id video ID. And now this will directly complement our pre-fetching here. So confirm it's trpc.videos.get1 pre-fetch ID video ID and in here trpc.videos.get1 useSuspenseQuery.id video ID. There we go.
We don't have to call this data. We can be more explicit. We can call it video here and let's do JSON, stringify and pass in the video. And now when you refresh here, Let's just import the video section from dot dot slash sections video section and pass in the video ID. And there we go.
Slight loading and then we have our user here. Perfect. So you should now be able to go to the studio, pick a video of yours and click here and it should load that here. Amazing. And you can see how fast it is once it's cached.
That's one of the really, really cool things about this tech stack is that it leverages the cache from React query, which is extremely fast when done right. But the app is also fast on the first ever load, which you will especially see in production. Keep in mind that if you do your own testing, you know, of the app and how fast it is. In production is gonna be even faster because in production, Next.js does prefetching of every link element that we use. And we use a lot of those.
I try my best to use them in every place that I can for that purpose. So it's much faster in production than it will be for you in development. And sometimes in development, it can be very slow, especially if the page is not compiled. So if you're visiting your page for the first time after starting your dev server, it might take a few seconds, if not even longer, because it needs to compile the page. None of that happens in production.
So yeah, just a quick tip about that. Now that we have the video section, we can go ahead and focus exclusively on this for now. And we already have one component that I want to use here. So let's go ahead and do this. I'm going to replace both of this with a fragment and I will open a div here and give this a class name of the following.
I'm going to add it a class name of CN from libutils, aspect video, background black, rounded extra large, overflow hidden and relative. And then if video max status is not ready, in that case, I'm gonna go ahead and render rounded bottom none. You're gonna see why in a second. It's so we can have a nicer display of our banner which will indicate to the user, hey this video is still processing. And now let's add our video player component which we handily created in our form section, right?
So we can go ahead and reuse it. Let's go ahead and pass some elements to the video player. Let's first of all add autoplay. OnPlay will be an empty arrow function. PlaybackID will be VideoMuxPlaybackID.
PosterURL will be actually thumbnail URL will be Video Thumbnail URL, like this. So now when you click here, it should automatically start playing. Perfect. So we now have our video player reused. Great.
But you can see that this video is quite large. It takes a lot of space. Don't worry because we will have some elements on the side later which will push it and it's going to be a bit more compact. Great! So what I want to do now is go outside of this div and create a video banner and I'm going to pass in the status of video mux status.
So let's go ahead and create this. It's going to be a very simple component. So video banner. Let's go ahead and create a type in the videos module. So types.ts.
We're going to go ahead and do the following. We're going to import inferrouteroutputs from trpc server and we're going to import approuter from trpcrouters app. So you should export this type approuter here in trpcrouters app where you add all the routers. And then what you can do is you can export type video, get one output and make that be infer router outputs, passing app router, and then simply navigate to videos, get one, that's it. So now we have the video get one output and we can use it as a type throughout our components.
So in the video banner which we started creating here let's go ahead and let's add that import so video get one output from dot dot slash dot dot slash types and create a proper interface here. Video banner props will use the status which will use the max status like this. And let's go ahead and let's import alert triangle icon from Lucid React And let's export const video banner here. Video banner props. Get the status.
If status is ready, in that case, we can just return null here. No need to render anything. Otherwise, we're gonna go ahead and render a nice little banner to indicate to the viewer that this video is still processing. So basically what we're doing here is we are doing the same thing that YouTube does when it shows that little yellow banner underneath the video which says, hey, this video is still processing. Let's surrender the alert triangle icon and let's give it a class name of size4, text black and shrink 0 and let's add a paragraph this video is still being processed simply so the user knows that the quality may improve later on.
Let's give this a class name of TextExtraSmall, MediumTextSmall, FontMedium, TextBlack and LineClamp1. So now go back inside of the sections video section, import the video banner from dot slash components video banner here, dot dot slash components video banner. And if you want to, you can go ahead and pass in the status here of waiting. And this is how it's going to look like. This video is still being processed like this.
And of course, It will also change here, right? This max status will be waiting as well. And then this is how it's going to look like. It's going to be slick, right? And let me just see.
So I'm using background yellow 500. If you want to, you can maybe make it a little bit brighter. I don't know, whatever you prefer. 400? Yeah, maybe 400 is a nice in-between.
But I'm gonna stay true to my source code. I'm gonna go with this one. Great, so just change this to video max status. And now if you go ahead and you create a new video here and you put a very, very long video, for that you're also gonna have to upgrade to max pro version so you can add longer videos and immediately publish it, right? And go ahead here, you should see the banner which says, the banner which says Processing, right?
Great, So that's the video banner component. And now let's go ahead and let's create the video top row and pass in the entire video inside because we're simply going to use a lot of things from video. So let's go ahead and build this component. VideoTopRow.dsx Let's start with the props. So import video.getOneOutput so it matches whatever we have here.
And then instead of here, export const video top row. Let's go ahead and assign the props here. Let's get the video. And now we're gonna go ahead and start building this part right here. So this is the top row, the title, all of those things, right?
Let's return a div with a class name, flex, flex column, gap 4, and margin top of 4. Let's add an H1 element, video title. Let's start the rendering the video top row from dot dot slash components video top row so we can also see exactly what we are building, right? Let me just close this. There we go, untitled one, two, three.
I just have to close this, there we go. I will do one thing. Okay, nothing. I was thinking about changing my auto play because I don't want my audio from my laptop to interfere with the microphone, but I just turned off the sound. So everything's fine.
Let's go ahead and style this H1 element by giving it a class name, text extra large and font semi-bold. So you should already be having a much much better look. Now below that create a div with a class name of flex, flex column, small, flex row, flex-column, small, flex-row, small, items-start, small, justify-between, and gap-for. And now inside of here we're going to create a video owner component. And you're going to pass in the user to be video.user and the video ID to be video.id, like this.
Now let's go ahead and let's build the video owner component. Inside of your components here create video owner.tsx. Let's go ahead and let's create the interface video owner props import video get one output here. And now let's go ahead and let's export const video owner, user ID and the video ID from, my apologies, user and video ID from video owner props. And now let's go ahead and build as much as we can with the information we have here.
So I'm just gonna go ahead and prepare a div saying user info here. And I'm gonna go back to the video top row so I can import the video owner from .slash video owner. And there we go, we now have user info here. So now let's go ahead and let's give this a class name. Flex, items center, small, items start, justify between, small, justify start, gap three and minimum width of zero.
Different breakpoints are basically different ways to render these kinds of things depending on if we are on a mobile or desktop device. Now let's add a link from next link so make sure you've added this import and the link will lead to slash users user dot ID we don't have this yet but we will have it in the future. Now let's go ahead and let's add a div here with a class name, flex item center gap three and a minimum width of zero. This is used so it resets whatever is the default width that this div would usually inherit. Now let's add user avatar from components user avatar.
This is a handy thing we created, I believe when we created our studio sidebar component. And let's go ahead and pass in the size, large, image URL, user image URL, and the name, username, Like this. Let me refresh a bit and there we go. You can see this is starting to take some shape here. Perfect.
So we have this and now below that let's add a span which will say zero for now subscribers. And let's give this a class name, text small, text muted foreground and line clamp one. And of course to do properly, fill subscriber count. There we go, zero subscribers, excellent. So now we're gonna go outside of the link here and what we're gonna do here is we're going to load the current user ID from useAlf.
We're going to do that so we check if we are the owner of this video and in case we are we are not going to show the subscribe button We're just going to show the edit video button. So if user ID is equal to user clerk ID, because this user ID which we get from here comes from a clerk package, right? So if you want to, if it's easier, you can remap this to clerk user ID, if that will make you understand this part easily. So if that's true, we're going to render the button component from components UI button, and then a link again with an href of slash studio videos video ID. And now inside of here simply say edit video.
Let's give this button a class name of rounded full and as a child prop and let's also give it a variant of secondary, like this. Otherwise, if we are the owner, if we are not the owner of this video, we're going to render a button again, but for a different purpose. We are going to render a subscription button. And while we are here, let's already create this button simply because I'm looking at its source code and we can definitely create it in this chapter already because it doesn't include anything that we don't have access to. So all we have to do is we have to create a new module.
So let's create a new module called subscriptions. And let's go ahead and create a UI inside and then just the components in here and create subscription button dot DSX. You're going to import CN from libutils and you will import the button and button props from components UI button. And then you can create the interface subscription button props, which will simply copy the on click from the original button, have the disabled, have is subscribed, optional class name, and size the same one as in the button props. Let's export consubscription button here and let's go ahead and destructure all of the subscription button props inside of here.
Once we do this, we will be able to simply return our button component. If we are subscribed, we will render unsubscribe. Otherwise, subscribe. Now let's give it all the other attributes by passing the size. The variant will be the following.
If is subscribed, we're going to use secondary. Otherwise, we are going to use the default one. Class name, we'll use CN and give it the default class name of rounded full and then pass whatever user decides to pass. OnClick will simply forward the OnClick and Disabled will simply forward the Disabled. There we go.
That's our subscription button. I thought it would be handy to create it in this part so later on it's easier for us. We can just import it. Let's import the subscription button here. And now let's go ahead and give it some props.
OnClick will just be empty. Disabled will be false. IsSubscribed will be false. ClassName will be flex none. And I'm going to move these modules separated like this.
There we go. So now if I refresh this, You can see that since I am the owner of this video, next to my subscriber count, it says edit video right here. So if I click this, I'm redirected back here. But if I log out from here, I should still be able to see that video. So I'm just gonna go, I'm gonna use the backwards button because I didn't remember my URL, but you can see that in here, now it says subscribe.
Great. So let me just go ahead and go back inside of here, inside of my videos, and then inside of my video ID here. And I can see that one thing's missing here and that is the user info. So let's go ahead back inside of the video owner. In here we have the subscribers and above that, we're gonna create a reusable user info component.
Let's go ahead and create it inside of the user module. We don't have users so let's go ahead and create a new module called users and inside of here create a new folder UI and then components. Inside of here let's go ahead and create userinfo.tsx. We're going to import CVA and Variant Props from Class Variants Authority here. And then we're going to go ahead and import our CNUtil.
And then we're going to import the Tooltip component, including the Tooltip itself, its trigger and its content. Now let's go ahead and let's create user info variants using CVA. If you don't remember we already use this, well we use it in a lot of components thanks to ShadC and UI but We developed avatar variants with it and we added all of these different sizes. So we're going to do a similar thing for the user info now because this will be a reusable component throughout the project. Let's add flex, item center and gap one as the default classes.
And then let's add variants here. Size. Default will use this targeter to target the paragraph and change its text to text small and it will target the SVG to size four. Then we're gonna have a size LG which will target this back to base, this to 5. And then we're going to go ahead and target the paragraph again, change font to medium, and it's going to target the paragraph one more time and change the text to color black.
And the last one will be small. This will use text extra small and size 3.5. Then we're going to add default variants, size, whoops I should have opened an object here, and set this to default. There we go, our user avatar, our user info variants are done. And now we can use them to create user info props using user info variants.
Like this. And we will have a name and class name here besides our size prop, which will now be inferred here. Let's export const user info now. We can use the user info props here. Like this.
Let's get the name, class name, and the size. And in here, let's go ahead and let's return a div with the class name CN user info variants and passing the size and the class name in case we want to overwrite something. Now let's add a tooltip component, a tooltip trigger and give it an as child prop so it becomes the paragraph here. Give this a class name of text gray 500, hover text gray 800 and line clamp 1. And inside simply render the name.
And then we're going to add a tool tip content and we are going to show the name again And give this in a line of center and a class name of background color black with a 70% opacity. So we're going to reuse this throughout the project whenever we want to display some user info. So let's go ahead and import user info here. And we are now going to pass the size to be large and the name to be user.name. And now when you have the user you will be able to hover like this and it will show the user's name.
This will be particularly useful if a channel has a long name so it's been cut off here and then you will be able to hover like this and it will show the user's name. This will be particularly useful if a channel has a long name, so it's been cut off here. And then you will be able to hover over it and see the full name. Great. So it looks like we need to align our subscribers and our title properly.
So let's go ahead and do this. Let's wrap these two elements inside of a div. And let's give this a class name. Plex, PlexColumn, Gap1 and a minimum width of 0. There we go.
Perfect! So let's go ahead and continue with our development. We finished the video owner component as much as we can with the data we have. So let's go back to VideoTopRow. After VideoOwner, we have to develop the VideoReactions component.
We still don't have the reactions API so we are also gonna do the best we can at this time. Let's add the class name to the wrapping div of the video reactions. Flex overflow X auto on small minimum width is going to be a calculation of 50% minus 6 pixels, small justify end, small overflow visible, padding bottom of 2, minus margin bottom of 2, small padding bottom of 0, small margin bottom of 0, and gap 2. I understand that typing classes in this way isn't exactly educational, but a lot of these things are simply tweaks that I did until I got the result that I wanted. The majority of these changes here are simply positioning it to be in this part of the top row and changing some breakpoints so it looks good on both the screen, on large screen with this opened, on large screen with this collapsed and on mobile screen.
Right? I always try to find a way to show what we're building, but sometimes it's just easier to type of the classes, right? And rather than explain exactly what I'm doing, because this is something that I already did. And now even I have trouble exactly remembering why I chose this, but these are mostly just tweaks to make it look good. And it will be more visible once we actually develop the component we need.
So we need the video reactions component and we need video menu component. Let's go ahead and quickly create both of those. So inside of the UI components, we are going to need video reactions.tsx. Let's export const video reactions. The video reactions like this.
And let's copy the videoReactions. And let's call this one videoMenu. And change this to videoMenu. And this will be videoMenu. Let's go back to the video top row and we can now import both of these components the same way we did with video owner.
I just like to order them by length. And now if you refresh, you should have no errors and you should see video reactions here and the video menu here. Great. Let's go ahead and let's start with video reactions. So we're not going to be able to do anything more than just, you know, like placeholder data, but it will be enough for now.
Let's start by giving this a class name of FlexItemsCenter and flexNone. Add a button from components UI button with a thumbs up icon and give this a class name of CN from libutils like this. And this is what we have to do now. So we're gonna have a prop called video reaction so I'm going to keep it here a video reaction and by default it will be liked like this. I think that's okay not liked it will be like there we go And we're basically going to use this constant as our mock data for now.
So in case, It will be size 5 by default, but in case viewer reaction is like, in this case, yes, viewer reaction, not video reaction, in that case we're going to paint this into fill black, like this. And then below this, we're going to render a number of flags. So you can put number one in here if you want to. And then let's add separator from components, UI separator. Let me just align my imports a little bit.
And we're going to copy this. And this one will be thumbs down icon from Lucid React. And this will do the opposite. It will check for dislike. And you can give this a type of like or dislike.
So you don't have type errors. Let's see. Okay, yeah. Let's just do this instead. If it's not like...
I mean, I'm just hacking this so it works with our mock data. And below this, the number of dislikes. So now you can see that we are starting to have some things here, but we do need to modify it a little bit. So this will be flex items center and flex none. And now let's go ahead and give this button a class name of rounded large full, rounded R none, gap 2 and PR of 4.
Variant will be secondary like this. And let's go ahead and copy these two elements. Let's add them here as well. And this one will be rounded L none and rounded R full. And it will use PL of 3 like this and it will not need the gap and the class the operator the separator will have orientation Vertical and class name height 7.
There we go. But it looks like we are not doing this correctly. Full, like this. There we go. So like and dislike.
Obviously this entire component is a bit to do properly implement video reactions. Great. Now let's go ahead and let's do the video menu component. The video menu component will basically be a dropdown menu. So let's go ahead and import the drop-down menu from here.
And let's go ahead and give it some props here. Like this, video menu props. It will have a video ID, optional variant, and also an optional on remove prop because we will be using the video menu component later in the playlists. So the video menu will be a very cool component because it will be used in this big screen But it will also be used in our grid here. When we got to displaying individual videos in a list in a grid here, we're going to reuse the video menu component.
And then later when we get to playlists, we're also going to use the video menu component. But we're going to need to give it a special prop on remove so that it can show that type of drop-down option. So this will all be a drop-down menu. And inside of here, let's start with drop-down menu trigger and give it an as child prop. Let's add a button from components UI button.
Inside of the button here, we're going to have more vertical icon. From Lucid React. And I'm just gonna move this to the top. Now let's go ahead and give this a variant of variant in case we want to change it for whatever reason. We of course have to destructure the video menu props here.
So let's pass in the video ID variant and on remove. Besides the variant prop on the button, we're also going to give it a size of icon and the class name of rounded pool. There we go. And now let's add the drop-down menu content and let's give this one an align of end and an on click event, event stop propagation. Now let's go ahead and add the drop-down menu item like this and let's add the share icon from Lucid React and let's give it an on click of a very simple empty arrow function.
And let's write share. Let's give this a class name of margin right of two and the size of four. Make sure you have imported share icon from Lucid React. Now I'm going to copy my drop-down menu item here, and I'm going to add Add to Playlist. And I will use the List Plus icon here from Lucid React.
After this, I'm going to copy it one more time and I'm going to add remove option with trash to icon from Lucid React as well. All of this will have empty arrow functions here. The difference with this one is that it's going to be optional. But let's go ahead and see how this looks for now. So I think that if I go inside of my studio here and find this, I should be seeing as you can see my drop-down menu right here.
Great! So let's go ahead back inside of the video top row and let's give my video menu here a video ID of video.id and give it a variant of secondary. Like this, there we go. Now, inside of this video menu, we don't really have much use of the video ID inside, but what we can do already is drag this on remove and only show it if we have on remove right so the last one should not be visible because we are only going to pass this prop conditionally if we render this component in a playlist, we will give it an on remove, which will actually remove it from the playlist and then it will have an option on remove. Great, we have that, We can leave this for now.
I'm of course going to add to do, implement everything, implement what's left maybe. And there is one thing that we can actually implement already here, And that is the on share functionality. So we can do const on share, and we can get full URL, the same way we developed it here. So just copy this part from form section and add it here. You can even add the same to do change if deploying outside of Vercel.
So this will just copy the URL. So you can use navigator, click board, write text, pull URL. And then you can post success URL, or we can simply say, link copied to the clipboard. There we go. And then you can use on share and pass it here.
So if you go ahead and click Share, let's refresh first. If you click Share, link copy to the clipboard, and if I paste here, there we go. Let's see if it works correctly. It should load the same video it does. Perfect, so we developed on Share.
What we have to do now is we have to go back instead of video.row and we can now finally develop the video description component. Let's go ahead and pass in description which will be video.description like this. Let's go ahead inside of the video description, which we first have to create video description. And we're going to create the interface, which will have a lot of items inside. Compact views, expanded views, compact date, expanded date, and description, which will be optional.
Let's export const video description. And let's go ahead and destructure all of those things. Now let's go ahead and assign them video description props. And in here, what we're going to do is we're already going to set the state. IsExpanded, setIsExpanded.
UseState from React and set it to false by default. Now let's open a div and let's pass in an onclick here to simply switch setIsExpanded by using current value and reversing the current value, like this. Now let's add a class name here, background secondary with a 50% opacity, rounded extra large, adding of three, cursor pointer, hover, background secondary with an opacity of 70 and transition. Inside of here, we're going to have a div with a class name of flex, gap of two, text small and the margin bottom of two. And now that I think of it, I'm trying to find a nicer way to handle this.
Because I don't want to pass the date through prop drilling, but at the same time I don't really like having this many props for a relatively simple component. But let's continue with what I have in mind and then we'll see if perhaps there is a better way of doing it. I'm going to go ahead and continue like this for now. Let's add span and the class name of font medium here. And now if we have expanded description, we will render the expanded views.
Otherwise, we will render the compact views and pass in views here. We will do the same thing for the date. IsExpanded, use the expanded date. Otherwise, use the compact date. Like this.
And then outside of this, add a new div with a class name of relative, and then add a paragraph here with a class name, which is going to use the CNUtil, like this. Let's give it the default classes of text small, whitespace, pre wrap. If it is not expanded, in that case, change this to line clamp 2. So we are not going to show the entirety of the description. And inside, render the description or pull back to no description.
Now let's go ahead and open one more div here with the class name of PlexItemsCenter, Gap1, MarginTopOf4, TextSmall and FontMedium. We are going to render dynamically. If isExpanded, we're going to render a fragment and the text show less with an icon chevron up icon from Lucid React and let's give it a class name of size 4 And let's remember to do the dynamic option here. We can just pass it here. This will be show more and it's going to use chevron down icon with the same size.
Make sure you have added all of these icons here. Now let's go ahead and import the video description from .slash video description. And let's go ahead and pass in all the information. So compact views here in this case will be zero. Expanded views will be zero.
Compact date will be, it can be something like this. It doesn't matter. Expanded date can be both Jan 2025. And let's just see what I forgot, probably the return. Yes, let's return the JSX.
Otherwise it cannot be used as a component as you clearly saw the error. Let's indent this, and now let's refresh this. And now, as you can see, we have kind of a, so this is how it's going to look like. Compact views will be 1.5K, whereas expanded views will be this, right? So 1.K views, and this is like the compact date, but when I click show more, it will expand the views, it will expand the date and also the description if we were to have one.
So if I go ahead, go inside of my studio here and if I create a multi-line description, a very, very, very long description. And I just copied a lot of times and I click save. And if I go here, you can see that it's limited to two lines. And then when I expand, it expands as well. So what I wanna do now is I want to create this compact views, compact date, all of those things inside of this component right here.
And I'm okay with doing it like this for one simple reason. Video description is not going to be reused outside of this video page. So for that reason, I'm okay with passing these things as props, right? Simply because I want to avoid passing the date object through props, Right? So this is what I'm going to do.
I'm gonna create the const compact views and I'm going to memorize it simply because this is something that will change So I want to make it optimized. The issue is right now, this doesn't even exist. So what we will do is return entl.numberformat, English, notation, compact. So we're gonna be using built-in API. We're not gonna develop our own.
And we're gonna start with pretending that we have a thousand views like this. So this will be the compact views. And then we're just going to copy this into expanded views. And it's simply gonna use the notation of standard. And then you can pass that here.
So compact views, expanded views. And it should achieve the same effect that we previously had. There we go. If you want to, you can even give it a little more creative number, maybe. There we go.
1.2 million and 1.245. Yeah. So you can see how it automatically uses the compact. You didn't have to worry about 1K or 1M. The compact API does it for you.
And now we're going to do a similar thing for the dates. In order to do that, let's just go ahead and import two things, format and format distance to now from date FNS. And now we're just going to create const compact date, which will be useMemo this, and it's going to use video created at. And we can return format distance to now, passing the video created at, and add suffix like this. And now let's create expanded date, which is going to use format with the following format date, months, year.
And let's go ahead and pass in the compact date and let's go ahead and pass in the expanded date. There we go. So now by default, it will say about two hours ago, whereas if I click show more, it will tell me the exact date. So pretty similar to how YouTube's description box works. I'm quite satisfied with this result.
Still a little bit unsure if I like this prop passing here or not. To be honest, we probably don't need this many memorization here, maybe for views, because views can change, but dates, I'm not sure if date really needs this, but you know, you take it as a practice. You know, we are learning how to use this native JavaScript APIs for numbers. And we are also learning how to pass props in this way, right? It would be the one thing that I would definitely advise you not to do would be running these functions directly here.
I would not advise you to do that. At least put it in a constant. You don't have to memorize it but at least put it in a constant. Great. So now let's go ahead and let's just create our sections here on the bottom and here.
So we're not going to do any UI. I just want to fill the area so we have the comments and the suggestions. And then we are done with this chapter. So let's go ahead and do that. So let's go back inside of our modules, inside of our videos, UI, videos, video view.
And below this, we're going to add a div with a class name extra-large hidden on the mobile it's going to be visible and the margin top of 4 and add a suggestions section like this Let's go inside of sections and let's create suggestions-section.tsx. Let's export cons suggestions section and simply return a div which says suggestions. That's it, nothing more. You can immediately copy this and add comments section. Let's rename this to comments.
With two Ms. There we go. Let's go back inside of the video view and let's import the suggestions section like this and then below it, add the comments section. And then outside of this div, add a new div, which will also render the suggestion section. You're gonna see why we are doing suggestions twice in a moment.
And let's give this wrapper a class name of hidden by default on extra large devices block. So basically the reverse of what we do here. A full width on extra large give it a width of 380 pixels. To Excel give it a width of 460 pixels and let's give it a shrink of 1. And now, if I've done this correctly, this should be in this area.
Let me zoom out a bit. There we go. So I was too zoomed in. This is the desktop views. This is the desktop view.
So we're going to have the video view here, the suggestions here and the comments below. But in case we go into smaller devices like mobiles, suggestions will be just below our video. Right? Or if we are in this kind of mode where we simply have no space to load suggestions on the side, which can be improved if we close this. But if we open it, sometimes it will push it below.
Great! So we have this. I'm okay with this large view. And basically what we have to do next is we have to implement views because right now they are completely mock. We have to implement reactions and then we can go ahead and go inside of comments and suggestions.
Before we can do comments though, perhaps it would be okay to do users first. I don't know, we'll see. For users, we are missing the subscription functionality and we are missing the actual user page. So I'm going to see if we are able to develop the comments without those features from the users. If we are, great, because I want to wrap up this entire page before we go into any new modules.
Great, so this was a longer chapter, but it did establish a very, very good bone, the skeleton for our video page. And we got a lot of here done. This, what I would say the main view is done, but it is missing a actual API for this. So that's something that we're gonna have to focus on next. Nevertheless, great, great job.
Let's go ahead and check everything we've done. So we created the get one procedure. We use the inner join to get the outer information. We did our pre-fetching process. We added the video section, comment section, and suggestion in form of placeholders.
Amazing, amazing job.