In this chapter, our goal is to create the basic comments functionality underneath our video section. The functionality that we are going to complete in this chapter will be the input form, which we have right here, the total count of the comments and basic comment display, which means that we are not going to implement this part. We will not yet implement the replies and the reactions. For that, we're going to reserve another chapter. The goal is simply to add a form, display a comment and update the total comment count.
In order to do that, we're going to have to create a comments schema, comments procedure and comments section. But before we do that, I want to wrap up a few things from our last chapter so that we don't forget to do them. The first thing we can already do is we can implement proper loading for this part right here because effectively, this entire part is finished besides, of course, playlists. We will work on playlists later when we develop that entire feature. So let's go ahead and go inside of our modules, inside of video, UI, components and let's go inside of video top row.
And let's go ahead and let's import skeleton from components UI skeleton. And from here, I'm just going to export const video top row skeleton. Depending on how important loading states are for you, you can choose to skip this part if you find it repetitive or feel like you don't really get too much useful information from this. But I'm gonna go ahead and build it anyway to have a complete product at our hands. So basically, I am just copying what we do here, and I will symbolize the elements going on.
So let's add flex, flex column, gap four, and the margin top of four. Now let's add a div with a class name flex flex column gap two and now let's add a self-closing skeleton tag with the class name height of six width of four four fifths is that how you say it in English four fifths and two fifths right here and now let's go ahead and add a div here with a class name flex items center justify between FlexItemsCenter, justify between, and full width. Now, another div with a class name of FlexItemsCenter, gap three and the width of 70 percent. Inside of here, let's add a skeleton with a class name of width 10, height of 10, rounded full and shrink 0 Below this skeleton, let's add a div, flex, flex column, gap to, and width pull. And let's add two skeletons here.
One will have a class name of height 5, width of 3 5ths, actually 4 5ths like this. On medium it will be 2 6ths. I hope I'm pronouncing this right, otherwise I'm just sounding very funny. So let's go ahead and do the same thing, but with different measurements for the bottom skeleton. And let's go ahead and add another skeleton here with the class name of height nine, two sixths and 1 sixth here and rounded full and just one more self-closing tag here height of 120 pixels and width full.
This represents our description which we are just going to leave an empty space for. So we are basically mocking this is a TypeScript error which will go away after I close and reopen the file or just restart VS Code. Basically, this will be our skeleton representing this content right here. It represents the owner component, the reactions, the menu and the description. So now that we have VideoTopRowSkeleton, let's go ahead and create VideoPlayerSkeleton, which is going to be way simpler, just one line.
This will just be return a single div with a class name, aspect video, background black, rounded, extra large, overflow, hidden, and relative. Actually, we don't need any of those. We can just do these three. Make sure you return that div. And now let's go directly inside of sections, video section here.
And let's create the proper const, video section skeleton. And Let's go ahead and return inside of fragments here Video Player Skeleton and Video Top Row Skeleton Make sure you've added these two imports from the relative imports respective imports and let's go ahead and replace this with that there we go so that's pretty much what's happening here so now when you refresh you should have a much nicer skeleton right here perfect So now let's go ahead and let's develop the comments. Actually, just one more thing. If you go ahead and visit your Bun Drizzle Kit studio, inside of your subscriptions here, you will see that our viewer and creator are called viewer ID and creator ID. That's because inside of our schema, in subscription relations, that's how we call them.
But we don't have to call them that way. If you take a look at any other relation, we are more descriptive, subscriptions, subscribers, videos. So we can do the same thing for subscription relations. Call this one a viewer and call this one the creator. And the cool thing is that I think just by refreshing the studio, it should maybe not by refreshing but restarting it.
I think we don't have to push these changes. If I understood how this is updated correctly, I think just restarting it, There we go. So just restart Drizzle Studio and you should see viewer and creator here. There we go. So I just wanted to have that finished.
If you really want to, you can try doing Drizzle Kit push, but I'm pretty confident that absolutely no changes will be detected because this is all on application level not on the database level exactly no changes detected. Great! Now let's go ahead in the schema here and let's develop the comments table. So I'm going to do the comments just below the videos because they will be closely related. So export const comments pg table comments.
Let's add id which we can copy from video it will be a normal UUID. Now let's add user ID, which will be a UUID, user ID, with references to user's ID and on delete cascade. It will also be required. Now let's add a reference to video ID, which will be our video ID, reference to videos ID on delete cascade, That's also good. Now the value of the comment will be a very simple text with value and it will also be required.
And now let's just copy these two, create that and update that. And now let's go ahead and let's push these changes here. So if you go to Bonnex Drizzle Kit Push, it should update the schema. Let's just wait a second. There we go.
Changes applied. Now let's go ahead and let's add the relations here. Export const comment relations, relations, comments. We will use both one and many. So we are doing this again only to improve application level understanding of our relations, not actually required if for whatever reason you find yourself having problems with this, you will be able to continue the tutorial, don't worry.
If you can't follow, follow along. So this will be comments user ID and it will reference to user's ID. And just make sure that when you return this you wrap inside of parentheses here so you return an immediate object and now it should look like this. Beside user we also referenced a video so that's going to be videos comments video ID and this will be videos ID. There we go and at the moment we don't have many so just leave it like this for now and now instead of your video relations you should have comments many comments, and you should have users, user relations, comments, many comments, right?
So both the user and the video from now on will have many comments, Great. So now what I'm gonna do is I'm going to just run, you know, just in case, even though I mostly use this to prove a point, whenever you do those relation API changes, it's just for application level. It's just for DrizzleKit Studio to have a nicer understanding and a nicer preview of what's going on. And also, if you ever decide to use Drizzle's relational API queries, then it will be required in that case. So now if I go on to comments here I have let's see user and video there we go so that's our comments schema perfect.
Now let's go ahead and let's go inside of our modules and let's go ahead and create comments module. Inside of here we're going to create a server and we're going to create procedures.ts. I'm going to copy some simple procedure we have like this one in video views. Simply so we have a boilerplate so we don't have to write everything. Let's add this to be comments router and import comments.
The create method can stay the same. The video ID will actually stay the same here. So what we are going to do is the following. Let's go ahead and how to remove everything from here. And this will simply be created comments, insert into comments like this.
And we will also need to add a prompt. Here's what we can do. I think we can do the following. We can go inside of schema and for comments, we can just copy this, these three, find comments and after comments relations, simply add those and instead of video, call all of these comments. So let me just fix this and this should be comments.
So comments, update schema, comments, insert schema and comments, select schema. Actually, in order to stay consistent, it should all be singular, like this comment select schema, comment insert schema and comment update schema. And now I should be able to just use comment insert schema from database schema, which means that I should still have the video ID here and I should also have the value here. And then I can just pass the value from here as well. There we go.
But I think that I have to be careful here because I think that this procedure might fail because my comment insert schema will probably require me to use to pass in... Let's see, I'm not sure. Maybe I shouldn't do this like this. How about we change this? Let's go ahead and do this.
Z.object and let's add video id to be z.string UUID and let's just do the value to be z.string. I fear that the comment insert schema might be less productive here because it might break the input, telling us that we need to pass all of these things, right, like user ID, which is not something we are passing through the input, right, or ID, right? So I just wanna make sure that that's not the case. So yeah, let's do it like this for now. And this should be enough for our create method here.
And now we need the getMany method, which will be the base procedure, meaning that this will be available for anyone to use. So our base procedure will have an input as well. Video ID, Z.string, UUID, .query, asynchronous method. And all we're going to pass here is extract the video ID from the input. We can get the input from the query props.
And then simply fetch the comments, actually let's call this data, using await database. Just a second, Database.select from comments where equals comments video ID, video ID from the input. And we need to import equals from drizzle ORM and return the data. So we are returning the entire array of comments. I think this should be enough for a very simple example, right?
The comments router, we can create it and we can get many. Later on we're gonna have that proper pagination here but for now I think this should be just fine. Let's go ahead and add these to our source TRPC routers app. Let's go ahead and import comments router and let's assign comments comments router. So what we are going to do now is the following.
We're gonna go inside of source app folder, home, videos, video ID page and besides prefetching the video we will also prefetch the comments. Get many, prefetch, make sure you add prefetch, ID, my apologies, video ID, video ID, like this. And I will add a to do here. Don't forget to change to refetch infinite because later on this will be an infinite query. For now it's just a very simple, you know, get many query.
Great, so we have this and now it's time for us to go inside of source, modules, inside of videos, UI, sections and we have the comment section. So we're going to mark this as use client because comment section is rendered in the video view and nothing here is client yet. So only when we got to here is the beginning of a client section. And let's go ahead and define the data, or we can call these comments because we're on the front end, so this variable is okay. TRPC from the client, comments, use comments, get many, use suspense query.
And we should pass in the video ID, interface, comment section, comments, section, props, video ID, string. Let's go ahead and use this. Video ID, there we go. And then what I'm going to do is what I usually do is just JSON stringify the comments. There we go.
So now I should go ahead and pass in the video ID here through the prop. There we go. And what I expect to happen now, when I refresh here, is just an empty array. Exactly. Perfect.
Now let's go ahead and do what we always do. So export const comments section here, rename this to section suspense. Like this. This can use the exact same thing here. Like this.
And return suspense from react error boundary from react error boundary and comments section suspense and passing the video ID, video ID. Let's go ahead and add fallbacks here. Error. And this one will be loading. There we go.
I will just move this here and now we should just see loading for a brief second here. So now these two will load independently. The video part will load whenever it loads and comments will load whenever they load, right? So they're not going to block each other. You will be able to start watching the video even if comments are still loading.
But thanks to prefetching, which we do inside of the video ID here, it's both going to be blazingly fast in production. You're going to see that when we actually have the ability to demonstrate in production and when we have the ability to click on the video from here, you're going to see how fast it is. And when it's cached, it's well, instant, which is really, really cool. Great. So now what I want to do is I want to continue building in the comment section suspense, more specifically, I want to build this basic form right here so that I can actually see some data appearing here to see if we created our procedures correctly.
Let's go ahead and go inside of source, modules, comments and in here we will create a UI and components here. And in here, a comment form, .tsx. So I decided to put that in the comments module, purely because the way we're going to build them is going to be reusable by default. So even though they will only be displayed in the video section, which could technically make them a candidate to have them in components here, right? Because it doesn't matter which entity it uses, it makes more sense for it to be in this module, right?
But since we're going to build them in a way that's reusable, I kind of feel having them here is a better idea. So let's go ahead and create an interface here Interface, comment, form, props Video ID will be a string And on Success will be an optional method like this. Let's export const comment form. Let's go ahead and destructure some elements here from comment form props. Those are video ID and on success.
Like this. And now let's go ahead and return a very basic form here. So I just want to build the UI. And since I already have this, how about we go inside of videos, UI, sections, comment section. And let's go ahead and just render that.
So I'm going to give this a class name of margin top of six. Then I'm going to go ahead here and give this a div of class name, flex, flex column and gap six, an h1 element which will simply say for now zero comments, simply because we didn't add any count here. And then in here, we're gonna have the comment form component and we can go ahead and pass in video ID, video ID like this. So import comment form component. And now you should see the text which says zero comments here and then in here you should see the empty array, which is basically JSON stringification of the comments here.
Great, so inside of the comment form let's go ahead and build the UI. So the form itself will have a class name of flex gap for and group. We're going to render the user avatar component from components user avatar. We're going to give it a size of large. We're going to give it an image URL of user image URL which we can get from our user from use user from clerk next JS so user and then question mark avatar or image URL.
There we go. And name will be user, username or user. And let's go ahead and use a placeholder SVG here, which I think, all backs to, let's see, placeholder, it's this. I think it's actually okay. I don't know, maybe I'll find some other image and I'll show you how you can download it from the gist, but let's use this for now.
So that's if we are logged in. So let's see that. See, yeah, it looks horrible when I'm not logged in. Okay, we will find some other solution for this, But let me log in now actually, just so I can see how this looks like. There we go.
My user avatar is here. Great. And now let's go ahead and add a div here. And we're just going to add a text area from components text area. We're going to give it a placeholder of add a comment and the class name of resize none, background transparent, overflow hidden and the minimum height of zero, like this.
And then outside of this div, we're gonna add a new div with a class name, justify and gap to margin top of two and flex. And let's add a button here, comment. Let's import the button from components UI button, like this. And let's give this a type of submit let's give it a size of a small like this all right so I think we did something incorrectly here so let's see yes I think that we should have wrapped this entire thing. We should have wrapped this entire thing together in a div like this.
And give this a class name of Flex1. There we go, that's what I wanted. So in here we have the user author, in here we have the comment section, and then we have a comment button, which right now does a hard submit on the form. Great, so let's go ahead and do the following now. I just want to add the proper placeholder because right now this just looks very bad when user is logged out.
So I have added a new SVG to our public gist, or you can simply access the source code and you will find this image. I used AI to generate this, but it's a very simple silhouette. So just go ahead and copy the SVG. And let's go inside of public and let's add user-placeholder.svg and add it here. There we go.
Let's go ahead now and go back inside of our comment form. And let's use placeholder or user placeholder, SVG. Like this, and there we go. So this now looks much better. It looks like the user is actually signed out.
And if you want to, you know, make this a little bit more attractive, you can just change this color. You like some other color. Maybe you will like this better. I think YouTube has something like this. Maybe it's this color, maybe it's not.
Yeah, so just choose whatever you like for your logged out user. And now let's go ahead and actually develop the comment form. So in order to actually do this, we first of all need the usual suspects, Zod resolver, whoops, Use form. We are going to need toast. We are going to need Zod.
And I think that should be it for now. Let's also add useClerk from here so we can prompt on error. This also means that we are going to need client TRPC. We can also go ahead and use comment insert schema from the database, though we have to be careful. So we're going to omit some things from it.
We already have the text area, that's great. And now let's wrap it up by adding the elements from form. Form, form control, form field and form item. So let's go ahead and define our form. Use form and let's give it the z.infer oops z.infer like this type of oops like this type of comments schema comment insert schema And then let's add a resolver here, Zod resolver, comment insert schema, and let's just omit user ID true that's not something we pass from the frontend.
Other things are okay like video ID that's something we will pass from the frontend and value is something that will be empty by default and something we will pass from the frontend. Great! And now let's add const handle submit which will have the values of zinfer, whoops, values zinfer type of comments insert schema like this and instead of here we're going to create our create mutation from DRPC comments create use mutation. So we can just pass in create, mutate and pass in the values. And we should have no errors because these two are compatible.
Great. Now let's go ahead and see what else can we do here. So this video ID should be automatically filled from the prop here so that's why I don't think we have to manually pass it here. So I think all of this should be fine But let's go ahead and do the following. Let's go inside of here.
Let's define const utils trpc use utils here so we can refresh things. So on success we're going to go ahead the utils comments get many, invalidate, or specific video ID. And let's do form reset every time we successfully submit. And let's do toast success comment added. And then if we want to, we can call on success callback in case we want to do something in an outer component using this method.
Great, now let's add onError. And we're going to leverage the error. If error data code is unauthorized, we will simply do clerk open sign in. We have to define clerk, so let's just do that const. Clerk use clerk.
Let me move here. Open sign in. And let's toast error, something went wrong. There we go. And now we should be able to continue building this so this is how we are going to do it We're going to wrap the entire thing in a actual form from our form component like this.
Let's indent it. Let's spread the entire form like this. And now let's go ahead and give this native form an onSubmit method. OnSubmit form handle submit and then passing our handle submit now let's go ahead inside of the div flex one and let's wrap our text area inside of a form field with a self-closing tag, which means we need to pass it through render like this. So now we can pass the text area inside.
The name of the form field will be value and control will be form control. Now let's wrap the text area inside in a form item like this. Then let's add form control And we will not have any form labels here. There we go. And let's now spread the field here.
And if you want, you can also add form message here, which is something we have to import from components.ua form, which will display any errors on the UI side. This can sometimes help you debug things. Every time I decided to hide this message, I got very cryptic, non-submitting forms, and I didn't understand what was going on. Because if you try like console logging this, it will never console log. That most likely means that this part detected a Zod error here.
For example, in my original source code, I forgot this And then it always expected me to manually add the user ID through the form. So that's why it never worked, right? So let's go ahead and try this out now. I believe that now, if I refresh this and yeah, if I try typing something now I'm getting an error like something went wrong and let's also do this disabled if create is pending so yeah if you try and add a comment while logged out you will have a nice error But if you go ahead and log in and go back where you can comment and try commenting, let's see. There we go.
I can add comments. Amazing. You can see how it immediately refreshes and we can see that the video ID value is passed safely through this initial default values which we used, which I'm very happy about. So we don't have to, you know, usually this is what I did in my source code, but now I changed it once I looked at the code. I did values and then I passed the video ID manually, right?
Because I thought that it did not infer it, but it did. Like this works just fine. The video ID is simply, we are using shorthand, right? So video ID basically gets passed through the props here and we render the comment form here in the comment section which definitely has to have the video ID because it couldn't even load the comments in the first place. Great!
So now that we have that working, let's go ahead and let's actually render these comments in a nice way. So let's go ahead and iterate over these comments right here. So what I'm going to do is I'm going to add a div with a class name flex, flex column, gap four and margin top of two And how about I move this right below the comment form, right, so still inside of this div here. And then inside of here, we are simply gonna go over the comments, get the individual comment and render a comment item component. The key will be comment ID and the comment will be the comment entirely.
Now let's go ahead and implement the comment item. So I'm going to go ahead and build that in the comments module as well, simply because the nature of it is so reusable. So let's go ahead and add comment item.tsx and let's create an interface comment item props. And the comment itself should be properly typed. So let's go ahead and go inside of comments, create types, not a single type, but types.ts.
So it should be inside of the comments module like this. And it should be very similar to the videos types. So you can go ahead and copy it, paste it here and simply change from video get one output to comments get many output. And simply select comments, get many. Like this.
And now in here, you can import the comments, get many output, and then just number like this. And then that will eventually make it a single object because if you don't do this, it's gonna give you an array of them, right? So you need to pick like one from the list. Let's export const comment item. Let's go ahead and assign this here.
Comment and let's go ahead and return some nice information here. So we will need the following. Let's start by having a div wrap the entire thing and then inside, let's do the first part where we render the user avatar be wrapped in a flex and gap for let's make sure that we use a link which will redirect oops to slash users comment user ID like this. And I think here's the first issue we will encounter. Let's import user avatar from components user avatar.
The issue is now that we don't really have too much information about what to send, what to send to this component. So let me just select large here. And we now have image URL with nothing to pass and name with nothing to pass. Why do I say nothing to pass? Well, try it, comment dot, we only have user ID.
That is because we never joined the user. So let's go ahead and do this for now. I'm gonna go ahead and leave this as it is. And I will just import the comment item simply so we have it here. And right now what should happen is, oh, nothing.
Yeah, There are some things being rendered here but they are invisible. You can't even see them. So what we have to do is we have to go back inside of our comment procedures on the server right here And we have to modify the getMany so that it includes the users using a inner join. So let's add inner join here on users from database schema like this. And specifically, we are joining where comments user ID matches the users ID, like this.
And now instead of here, you should be able to spread get table columns of the comments schema. So import get table columns from drizzle ORM. And here, pass the user, or you can just do users. I think this should work just fine. So now if you go into comment item, you should be able to have comment.user and their image URL.
So now if I refresh this, there we go. You can see three images of myself because I'm the one who left the comment. Great, so that's how easy it is to use inner join here. Perfect. And this is precisely why I prefer using Drizzle SQL-like query builders over Prisma or over Drizzle's relational API builder.
It simply makes us, it forces us to understand our queries. Right? In Prisma, I don't even know what's the term anymore. We would just use include users true, right? We will have no idea.
Is that an inner join? Is it a left join? Is it a common table expression? Is it a sub query? We'd have absolutely no idea how it's doing it.
But using Drizzle, it forces us to either improve our SQL understanding or just improve our understanding of Drizzle's SQL query builders, right? So this is why I prefer this, especially for tutorials. I think we gain much more knowledge into building queries this way, rather than just having, you know, a magic include user true, like something like that. I understand that's more convenient, but I think the knowledge is unmatchable when it comes to this. Great, so we have that in the comment item.
So now let's go ahead and just add some more information. So outside of this link here, Let me just wrap this up. So comment user name, full name, name. Okay. Let's add a div here with a class name flex and the minimum width of flex1 and the minimum width of zero.
Let's add another link here with an href. Basically the same thing as we did here. I'm purposely separating the links Because if you just wrap the entire thing inside of a link, then the entire thing is clickable and you don't really want that, right? You don't want the entire comment item to be redirected to the user. And inside of here, we're gonna add a div with a class name, flex items center, gap two and margin bottom of 0.5.
And then a span rendering comment user name and give this span a class name, font medium, text small and padding bottom of 0.5. There we go. John Doe three times. Then below that you will add a span and format distance to now from date FNS. Inside you're going to pass comment created at like this and add suffix true.
There we go. Or maybe you can use updated at whichever you want, right? I think it makes sense to use this one. Now let's add class name here, text extra small and text muted foreground. There we go.
Now, outside of this link, you can go ahead and add a paragraph, comment value, and add a class name, text, small. There we go. I can now write a comment. So if I click comment, there we go. I can now write a comment.
Perfect. So I think this is exactly what we said we are going to achieve, right? Just this part right here without the replies, without the buttons here. So we created the comment schema, the comments procedure and the comments section. Great!
We will also do this in the next chapters. What I want to do specifically in the next chapter will be infinite loading because right now this is not infinitely loaded. And after that we have to handle likes and dislikes which will work identically to this method here and we have to add replies which will be quite easy. I accidentally clicked on a user so it will be quite easy to develop replies simply because we have all the components finished. We are going to reuse this component and we're going to reuse the comment item component.
We'll just give it a variant prop which will hide this icon if needed and give this a cancel button so it closes. And then we're also going to have to add some more procedures. So I'm going to add this in the next chapter and the infinite loading in the next chapter as well. Great! So I believe that's it and we can focus on other things in the next chapter.
Great, great job!