In this chapter, our goal is to create a video form. We're going to use this video form to edit the title and the description of a video, and later on also modify the category, visibility, thumbnail, and use AI to generate the description or the title. We will also have this little drop down here which will be used when we want to delete a video, right? And inside of this place right here we're going to have a video player. Let's start by adding the skeleton to our previously created videos section simply so we don't forget that because right now we only have this loading state here.
So I'm gonna go back inside of my videos section. It's actually right here inside of the studio module and I'm gonna go ahead and create videos section skeleton component this skeleton component will have a fragment and then a div inside of here I'm going to add border y and Then I'm going to copy the beginning of my table, which means up to the table body. I will copy it like this and then I have to add a closing tag for the table body and for the table itself. And now I should have no errors in here, I believe. Let's see if this is correct.
I have to indent this. All right. So basically what I want to do now is I want to add this to my suspense fallback. Now when I do a hard refresh, I should at least see, as you can see, the table here matching. And now let's just add some simple skeletons for the data here.
So for the data, we're going to go inside of the table body, and we're simply going to use the array from length 5 or you know whatever you think looks good. Skip the first argument, get to the index and then let's add table row here. We're going to use the index as the key. Let's add a table cell here with a class name pl of 6. Let's add a div here with a class name of flex item center and gap of 4.
And then let's add a skeleton component. You can import the skeleton component from components UI skeleton. Like this and let's give this skeleton here a class name of height 20 and width of 36. So now, there we go. You can see how we have a nice skeleton, which kind of represents our thumbnail here.
Great. And now what I wanna do is I want to add additional items here. So below this skeleton, let's add a div with a class name here, flex, flex column and gap two. And inside, we're just going to add two skeletons like this. One with a height of four, one with a height of three and some different widths for them.
So now they will represent, as you can see, the title and the description here. Great! And if you want, you know, you can leave it like this, but let's go ahead and let's just add some dummy skeletons for the rest of our table cells. So open a new table cell here, and let's just see. So we have video, visibility, status, date, views, comments, likes.
So let's add visibility here. Then after visibility, we have status. Then we have the date, views, comments and likes. So three more. Views, comments and likes.
Okay, so now we filled all the available properties here and now we can just add some skeletons inside. So for this one I will add a skeleton with the class name height 4 and the width of 20. For the second one, the status, I'm just going to add another random one with the width of 16. Then for the date one, I'm going to add one with a width of 24. Then for my views, I'm just going to add a random one with ML auto, and I'm also going to give this table cell text right, and I will copy that and add it to the last two here and you can actually copy the skeleton as well for the comments and the likes here.
There we go So now you should have a nicer skeleton. Let's go ahead and just add these elements, text right, to my actual elements here which I believe are views, comments and likes. So all of these should have, whoops, last name of text right and let's give them text small as well. There we go, so all of them should be kind of in the end here, except the last one which will have PR of six here as well. There we go, like that.
So how about we add PR six here as well. PR six, I'm just trying to match my skeleton as much as possible for, so I'm choosing the last place here, which is the likes, right? And I think I'm missing P6 in this place right here. So how about we go and find the table cell and give the table cell a class name of PL6. And now, there we go.
This matches exactly what we have there. Great. And we already did PL6, I believe, here in the table cell for the skeleton. Perfect. So now, we're going to go ahead and develop the page which opens once you actually click on one of the videos.
So that's slash studio slash videos and then the video id. If you click right now you will get a 404. So let's go ahead inside of source, inside of the app folder, studio, studio. Let's go ahead and create our videos and then a dynamic video ID like this and inside a page.tsx. Let's return this page video ID like this.
Now when you refresh you should see video ID text here. And every time you click on one of these videos, you should see video ID. Great, So in order to actually develop this page, we are gonna have to create a proper prefetch method here. So let's go back inside of Studio procedures here and we're gonna develop a getOne method. So we have getMany protected procedure, and now we're going to call get one.
Why am I adding this inside of studio router procedures? Why not inside of video procedures? Simply for the reason that usually inside of my videos procedures, anyone will be able to visit a video, right? But this one, I want it only to be visible by the author of the video. So then we have separate routers and in one place we can show much more info than we would in another.
So that's why I decided to use it here. So the input here is very simply going to be an ID of the video which is you know I put .uuid here simply because I know that in my schema I use video uuid. In your case you can just put z.string, it's gonna work just fine. It depends on how precise you want to be. Great, so we have z.object set here and then let's add the query.
So now let me just collapse these elements here, input and then the query and this will be an asynchronous method. From here we can destructure the context and the input like this and let's go ahead and obtain from context.user, which is our database user, if you remember, id user id. And from the input, we can destructure the ID. There we go. So let's go ahead and let's fetch our video which will be await database.select and pass in from videos inner join.
My apologies, no need for an inner join here. We're just gonna pass in where, and then we're gonna add end, because we need two things to happen. The first one in our end clause here will be that the videos.id are equal to the id which we have from the input here and the second one will be that the videos user ID is equal to the current user ID. So that way this video that we load must belong to the user. Let's return back the record.
My apologies the video. In case there is no video we can go ahead and throw new PRPC error, which we can import from the server, passing the code not found. There we go. So we now have our GET1 procedure here. So How about we go inside of the page here and we prefetch that.
Let's mark this as an asynchronous page. Let's not forget the expert const dynamic, force dynamic, because we don't actually await anything. We just prefetch. So Next.js doesn't know that we are actually doing some fetch calls here, which cannot be static. Let's create the props so we can add the proper params from here.
Like this, page props, params. Let's go ahead and let's await video ID from await params. And now inside of here get one we can simply pass in the ID to be video ID. And there we go. We successfully prefetched our video here.
Great. So what we have to do now is we have to hydrate the client. So let's add hydrate the client from the RPC server like this and then we have to create a video view like this and we have to pass in the video id to be video id from the params. Great! Let's go ahead and let's create the video view.
So I'm gonna go ahead inside of my modules here and I'm going to say studio, UI, view. Oh this should be called views so we are gonna resolve that But let's first create the video view.dsx. And inside of here, let's just add page props here. Video ID will be a string. Expert const video view, video ID, page props.
So these will already be awaited. So no need to do it here again. Let's just return a div with a class name, px of 4, padding top of 2.5, and a maximum screen length of LG. And we are not gonna add MX auto. Usually we add that, but this time I don't want it to work like that.
And let's just render the video ID inside. Then let's go ahead and go back inside of our page in the app folder here and let's import the video view from Modulus Studio UI view video view. So now when you refresh you should see the ID of each of the content that you press on. Great, so that works. Now let's go ahead and let's continue developing but I just want to rename my UI folder view into views.
And if it asks you to update the imports, you can just say yes. And let me see what did it update now. All right, I'm trying to find some places. Oh, okay, so this is the place it updated. Instead of source app, studio studio page.tsx, where you import the studio view, just modify it to be views, in case you have the same mistake as me, right?
The place where you prefetch for studio get many. Great. So we now have that. Now let's go ahead and let's continue developing inside of the actual video, my apologies, views, video view here. So we're going to create a new section, which will be called form section.tsx.
And instead of this form section, we're gonna mark it as use client, expert constant form section. The form section will have page props, right? You can call it form section props, however you want, it really doesn't matter. So we only needed the video ID here. And basically what we're gonna do is what we always do.
We're going to go ahead and get this one video by using trpc from the client calling the studio get one use suspense query and pass in the video id my apologies id video id. There we go And let's go ahead and return the JSON stringify video. There we go. So now I'm going to go ahead and render the form view, my apologies, form section component from dash dash sections and passing the video ID here. There we go.
So now you should be able to see some more information inside of a JSON in the page here. So now let's go ahead and do what we always do inside of our sections where we fetch something and that is to not export this and rename this to suspense and then export const form section which we'll call the suspense and error boundary, form react error boundary, and then form section suspense here. And we have to do the prop passing. So let's go ahead and do that and passing the video ID, video ID. The fallback here will be an error and the fallback here will be form section skeleton.
Let's just fix this typo here like this and const form section skeleton for now can just be a paragraph loading. And let's not forget to return this. There we go. So that should now be working just fine. Now when I refresh it says loading for a brief second and then it shows this but we seem to be having an error.
Switch to client-side rendering because the server rendering error unauthorized. This would indicate that we made a mistake in our pre-fetching rules. It would mean that we are fetching something incorrectly. Let's go ahead and confirm. So we are calling trpcstudio get1, we are calling useSuspenseQuery with the id of video ID.
And inside of our page here in the studio, videos video ID page, TRPC Studio get one. Oh yes, we are not prefetching. There we go. So now we have no errors. Now we are doing this exactly as we are supposed to do.
Perfect. So that was the issue. You can see how careful you have to be. That's why, again, you know, I keep bringing us back to this tier pieces setup where I told you that we have to be careful with how we prefetch and everything. And I showed you that example of what the creators of React query have planned to kind of give you some strict rules about that.
That's why it will be that useful when it comes out. All right, so just make sure you're actually doing the prefetch here and then you will not have any errors in this page. Perfect! So now that we have this, let's go ahead and let's create a little div here with a class name of FlexItemsCenter, justifyBetween and marginBottom of 6. And inside of here, I'm going to add another div which will simply hold my heading VideoDetails and it will hold my description manage your video details and I will change this from an h1 element to a paragraph.
There we go video details and manage your video details. Let's give the heading element a class name of text to Excel and font bold and let's give the paragraph a text extra small and text muted foreground. There we go, that looks nice. Now let's go ahead outside of this div and add a new div with a class name flex items center and gap x of 2. And let's go ahead and add a button from components UI button so make sure you have added that, which will say save.
This will be a type of submit. And let's go ahead and give it a disable of false, simply so we remember later on that this should probably be controlled by something. Great. Now we have a save button here. And here's how that's going to look on the widescreen.
We are not going to center it. We're just going to leave it in this part. This is the same solution that the actual YouTube uses, YouTube Studio uses. Great. And now what I wanna do is I wanna add a little dropdown menu right next to our button.
So let's just import everything we need from the dropdown menu. Dropdown menu, the content item and the trigger. So now we're gonna go next to this button here And we're gonna go ahead and render the dropdown menu. So dropdown menu, dropdown menu trigger as child. And let's give it the button.
The as child basically means that this component will become its child, right? Otherwise this will be a button and then this will be another button inside which breaks the hydration rules. Give this a variant of ghost and a size of icon and add more vertical icon here. You can import this from Lucid React. I will move it here.
Now that we have this, let's go outside of the dropdown menu trigger and add a dropdown menu content here within the line of start and add a drop-down menu item and add inside a trash icon from Lucid React with a class name of size 4 and mr of 2 and then simply add delete there we go, so now next to this you should have a little drop down here, let me just see can we maybe push it so it's here to the side? We added an align start. How would I add side left, maybe? Okay, so that works like that. Not exactly what I imagined.
Align end maybe and leave the side alone. There we go. This is what I wanted. I don't want it to be in this place. Perfect!
So we have that ready and now let's go ahead and let's actually add our form so that we can actually load the details of our video inside of a form. So in order to do that, we are first going to have to import from react-hook-form. So let's go ahead and add use form from react-hook-form. We already have this installed thanks to chatcn ui. In case you don't, let me show you my package json here, react-hook-form.
Then let's go ahead and let's add Zodd resolver, which is again the same thing that we got when we installed everything from chat-cn because when you add form component from chat-cn it will install all of these things for you. Excellent, so we have that and now we are going to need an input from components UI input in order to display our text. We will need a text area in order to display description. And we're going to need all these other elements from form. So from components UI form, form, form control, field, label, message and item.
And we are also going to need everything from select. Select, select content, item, trigger and value. Great! Now let's go ahead and let's properly develop this. So we're going to start by establishing our form inside of a constant here.
So const form will be useForm like this and we are going to pass in the default values to be video. Like this. Now the issue is that this form is currently not strictly typed exactly. So it does use useFormReturn but it has no idea what we will actually be expecting to send to our create request or in this case our video update request. So usually what we would do is we would create a form schema using Zod.
But if you remember we actually have our schema here. So what we can do is we can leverage something called Drizzle Zod, and then we can extend an export from this schema. Let's go ahead and do that. So I'm going to do bun add drizzle zod. You don't have to immediately type it out simply so you can see the version.
There we go. So this is my version. So bun add drizzle zod at 0.7.0 if you want the exact same version. And let's go ahead and let's import this instead of our schema.ts here at the top. I'm going to import from DrizzleZod, create insert schema, create select schema, and create update schema.
And then I'm going to go ahead and find my videos here. And at the end of the videos I'm gonna go ahead and just write out the insert all of the schemas right so export const video insert schema will be create Insert Schema and pass in the videos. Export Const Video. Update Schema will be create update schema from my videos and we will have Export Const Video. Select schema just in case we ever need it, right?
It doesn't hurt to have one. And passing the videos. So that's how we can reuse our schema all the way here from the schema place. And then let's go back inside of our form section here. And we still have to import Zod but we will not use it to build any form schema from scratch.
Instead, we're just going to use it to infer. So let's go ahead and give this a type of z.infer type of video update schema which you can get from database schema. I will just move it here. There we go. So video update schema.
Perfect. And then you have to add a resolver to be Zod resolver. We added an import for that and again add video update schema here. There we go. No more errors in here.
Now this perfectly matches the type of video update schema. Now let's add our onSubmit method to be an asynchronous method with data of z.infer type of video update schema and console.log the data here. There we go. So now our data will be a proper type which will be expected in our TRPC create method and it will also validate the form. Great!
So now that we have this, let's go ahead and actually build the UI elements. So we're going to have to wrap this entire thing inside of a form element which we've added an import for a few minutes ago and inside of this form element we have to spread the form which we have created right here like this and then we have to wrap the entire thing again but instead of a native HTML form element. So wrap the entire thing and indent like this. And now this type submit makes sense because it will call this form. But we still have to give this form an on submit.
In this case, form handle submit and then on submit. Handle submit will take care of validation and once the validation passes only then will it call onSubmit with our proper data inside so we can safely pass it to our server. So this is going to be first class client-side validation as well as back-end-side validation. Great, so we now have this. Let's go ahead and let's add our first, well, our first element, right?
So this is how we're gonna do that. We're gonna create a div, and we're gonna create a grid. On mobile, this will simply be a single column. But on large devices, we're going to turn this into grid cols of 5. And let's give it a gap of 6, like this.
Then we're going to have a first column, which will have space y of eight and an lg a col span of three. Basically what we're doing is the following. We're separating these two into columns. This part right here, oh it's this color, okay. This part right here and this part right here.
This will be column number one and this will be column number two. Right, that's what we're doing. On mobile they will just go one below another. Great, so let's go ahead and continue developing the first column which will actually hold our fields. So let's add form field which we've recently imported.
It's a self-closing tag which takes in control of form.control. It takes in a name which basically tells you what field this is going to control. So it's going to be title and then we need the mandatory render method. Inside of this you can immediately destructure the field and then you can go ahead and return a form item. The form item will have a form label and inside of here we're going to go ahead and simply write title for now and I'm gonna add to do add AI generate button.
Below the form label let's go ahead and let's add form control like this and let's add an input which is a self-closing tag and simply spread everything from field here. The way this works is that this field automatically has all the necessary events. Let me find them. Is it here? Yes.
OnChange, OnBlur, Value, Disabled, Name. So all of those things will automatically be passed as props here. So that's why it's very useful to build forms in this way. Let's give this a placeholder of add a title to your video. And let's give it the class name of BR10.
Actually, no, we don't need this class name. Great. And let's add a form message here so we can properly render out any UI errors from here. And already you should be seeing a loaded name of your video. Right now all of them are untitled.
If you want to, you can go ahead and go inside of your Drizzle Kit Studio. Make sure you refresh so you see most up-to-date data and change one to hello and click Save Change. And then go back to your content, refresh and one of them should say hello in the title and the other one should say untitled. Great! So we now have this.
Now let's do the same thing but for description. So I'm going to copy this entire form field here and I will paste it below. This form field will control the description and it will have a form label of description like this. And instead of using an input, we will be using a text area component and we're going to go ahead and override the value so we are going to check if value exists then render the value otherwise an empty string because description is not required. We're gonna limit the number of rows to 10 so it doesn't look too big nor too small.
And we're gonna give it a class name of resize none and PR of 10. And we're going to change the placeholder to be add a description for your video. And by default, both of these should be empty because none of our videos actually have any description. Great! So now what we're going to do is we're going to create a different type of form, which will be our select form.
So let's go ahead and copy the form field again. And this is what I'm going to do before I paste. I'm going to add to do add thumbnail field here. We're not going to develop that in this chapter. We're going to develop it some other time.
But yeah, in between these two it's gonna have a thumbnail field. So now I just pasted this copy the description form field and I will rename this one to category ID like this. This one will not have the special AI button and the label will be category. It's not going to have the text area. Instead, this is how it's going to work.
We're going to wrap our entire form control within Select, like this. The Select itself will have an OnValue change to call FieldOnChange, and DefaultValue will be FieldValue or Undefined. And default value will be field value or undefined. Then inside of form control, we're gonna have select trigger with select value with a placeholder, select a category. It's a self-closing tag.
And then outside of form control, we're going to have select content and then select item. And for example, let's say something and let's give this a value of something. And now you should have the category right here. But one thing that we don't have are the actual categories. We forgot to load them.
And one thing we can do is we can prefetch them alongside our user. So I believe that you already have everything necessary. If I go inside of my modules, categories here, we have GetMany. That's it. That's all we need.
So let's go ahead and do this inside of your app studio videos video id page alongside pre-fetching the video let's go ahead and pre-fetch categories there we go So now let's go ahead back inside of our individual form section, and we're just gonna do the same thing. So we're gonna go ahead and prefetch the categories. From trpc, categories, get many. Use suspense query and do nothing inside. Now, you're probably noticing something.
Our entire loading skeleton will now wait until both of these are loaded. So technically, if you want to, you can separate this entire form field for the category into its own component and then add its own suspense and error boundary if you want to. Because yes, technically, if video is already loaded, we will still wait for this to be loaded. So not perfect, but I think as long as I give you an explanation in how to what's going on, you can decide for yourself if you want to modify it later. But for the sake of simplicity, I'm just going to continue developing like this now.
So now I'm going to modify this to actually iterate over the categories. So category.mat, we're going to get the individual category, and we're going to render the select item. And then we're going to go ahead and pass in the key to be category ID and the value to be the same. And inside of here, category name. There we go.
So now you should have the ability to choose different categories for this video as well. Great! So let's go ahead and stop here simply so we can go ahead and actually submit the form and see some changes. So in order to submit the form we have to go ahead and find and we'll develop our update procedure. So let's go inside of source, modules, videos, server procedures.
We're gonna put it here because we have the create method here. So let's go ahead and add the update method here, protected procedure. And let's go ahead and let's add an input here. The input will be video update schema from database schemas, so we can share that as well, and we are calling a mutation. So you can see how we have fully synchronized our front-end form validation with our back-end form validation.
I really like how easy it is to do that with TRPC and with useForm. And now let's go ahead and let's distract the user ID from context user. In case we are, actually we don't need to check for that. This is fine. Let's go ahead and do the following.
Let's first find the video by using await database. Actually, we can just do this. Updated video, await, database, update, set. Of course, passing the videos here. And we're going to simply set inside.
We could just spread, you know, the entire input, but I wanna be a bit specific with what I want to allow the user to update because this is an API endpoint, but I want to control what's possible to update. So I will allow the title, I will allow the description. I will allow the category ID. And for the visibility, I'm not going to allow the... Well, I will allow the user to change this, but only if video max status, well, yeah.
Let's go ahead and let's allow the visibility as well. I was thinking about, you know, only allowing the user to change the visibility to public if the video has finished processing. But now remember, YouTube doesn't do that. YouTube allows you to publish a video even if it's processing and then the viewers will simply see a text saying, hey, this video is processing. So let's do it that way.
No need to complicate in that case. And we have to add a new date for our updated at. And now let's just add a where, it has the use and, we're gonna have two equals here. Looks like we have to import both and from drizzle ORM and equals from drizzle ORM, so I will move that to the top here and the first one will be checking if our videos id are matching the input id and the second one will be if videos user ID matched the user ID which we got from this protected procedure. And let's get back returning.
In case we don't have an updated video, we're gonna throw a new tRPC error from tRPC server code not found. Like this. And it seems like I have an error in my update procedure here. Input ID seems to be able to be undefined. Well, that's not exactly true, right?
Because I mean, I'm not sure. I think that we are able to like modify this and to tell it that ID should be required. But for now, let's just do, if we are missing input ID, throw new trpc error code bad request like this. There we go, no more errors now. Perfect!
So we now have a working update procedure. So we can go back inside of our form section here and inside of our on submit method we can now go ahead and add that. So first let's just add our update here. Const update is trpc videos update use mutation like this and then instead of here what you can do is you can first of all just await update mutate asynchronous and pass in the data. Of course you don't even have to make this asynchronous.
You can just pass in mutate normally, but then you will lose the fact that the form field has form state and you can read is submitting from here, right? So if you wanna use that field, you have to mark this as asynchronous. So depending on how you wanna do it, right? If you want to, you can use it like this and then you can simply use update is pending, right? Basically something to just disable the submit button.
I actually prefer using the update is pending over this one. So I'm gonna do it like this. Great, this should now work fine. Let's go ahead and let's just disable this button if update is pending. Like this.
And there are some things that we should do you know after we successfully update our video. So let's go ahead and do the following. In here on success, what I'm going to do is I'm going to use the utils on utils, trpc, sorry, use utils, trpc utils, use utils, okay. So I'm gonna add PRPC, my apologies, utils, dot, the one I want to invalidate is studio, getMany, dot invalidate. Studio, why is it not working?
Studio.getMany, this expression... Oh, I should not call it. So I just want to invalidate it. And utils, studio get one, invalidate specifically this one. Right, so no need to lose cache for all the other ones we already loaded.
On error here, you can decide what you want to do. If you want to display the error message, I think I've shown you how in one of the previous chapters, but I'm just going to bring back a generic error message instead. Sometimes it's best not to show the user all the information that's coming from your backend, especially if they're malicious. But yeah, with CRPC, you can rely on the error here, so you can use error.message if you want to. Great, so we have this here.
So how about I change hello to hello123 and click save. And go back here. There we go, hello123. Perfect. How about I do this and click save.
And refresh and go back. Looks like it's saving. Great! And I will just copy this here. Those to success.
And this one will say video updated. Great! So now I should also be able to change the category I believe. There we go, video updated. Perfect.
So all of this seems to be working just fine. And now we have to build the second column here which will display our video player, right? So we can see the current thumbnail. And also give us a form to change the visibility. So let's go ahead back and now let's build this second column.
So I'm gonna go down where we have I believe the last element here. I think this is the div where the next column starts. Sorry I have to look at my source code because it's a bit hard. I sometimes lose focus where I'm at. It's a lot of lines here.
I'm pretty sure this is where it ends. So you can go to the bottom of your file, find where the main form ends, when this ends, and then before you end this div, but after you end this div, start a new column with a class name of flex, flex column, gap y of 8 and enlarge col span 2. So now if I add a little paragraph here, hello, it should appear right next to my form but if I go into mobile mode it should just break down. Great, so you should also see it right next here. So let's go ahead and add a div here with the class name flex, flex column, gap for, background of F9, F9, F9, so kind of a gray color, rounded extra large, overflow hidden, and height of fit.
And inside of here, class name, aspect video, overflow hidden, and relative. And inside of here, a video player. Video player does not exist yet but we are going to create it in a moment. It's going to have two props, playback ID which is video mux playback ID and poster URL, which is video thumbnail URL. Like this.
Actually, we can rename this the thumbnail URL. Why wouldn't we? Right. It's our component. Let's go ahead and create the video player component.
So I'm going to create that inside of the videos module. So instead of UI components I'm going to have a video player.tsx like this And I'm going to start by creating an interface for video player props. The playback ID will be optional, string null or undefined. Same thing for not poster URL, but thumbnail URL. We will also have auto play so that we can decide whether we want to do that or not.
And we are also gonna have this onPlay method here because later on, that's how we're gonna count the views after the video plays, not just by a website visit. So let's go ahead and export const videoPlayer. Let's go ahead and use these props here. And let's go ahead and get all of them. I just have to properly name my poster URL to thumbnail URL.
And in case we don't have the playback ID we can just return null. I believe the video won't even be able to load in that case. So what we have to install now is we have to install mux mux player. So let's go ahead and do that. BunAdd at mux slash mux-player react.
You're going to see my version in a second. So if you want to, you can wait to see the exact version that I have. In that case, that's 3.2.4. So you could do something like this. BunAdd mux-mux-player at 3.2.4.
Great. Now that we have the video player, let's go ahead and let's import the video player. There we go. Let's go ahead and mark this as use client And let's go ahead and simply return the mux player. Mux player, it's a self-closing tag.
Let's give it the playback ID of playback ID, poster of thumbnail URL or slash placeholder SVG, player in its time 0. This helps with hydration. If you don't set this, you will get some hydration errors. Auto play simply to match our auto play boolean, thumbnail time 0, last name, full width, full height and object contain. Accent color, For this one you can basically choose what your like progress bar will be the color of.
So for example how about we do this one. FF2056 and let's pass in on play to be on play there we go so now let's go inside of the forum section and let's import the video player from modules videos UI components video player and let's refresh and let's see if we have any errors or was this just you know a single thing that happened here looks like it's taking some time Let me try and speed it up by closing this and running Banner Run Dev All. Perhaps that will be able to make it load faster on this refresh. And there we go. We now have a very nice video player here.
I will mute myself, I mean, mute the video just so we don't have a double audio, but we can finally preview our player here. We have captions, we have everything here. Looks very, very nice. Great, so now what we have to do is we have to continue developing this section and we have to add some more information like the video link, the status of some things and we have to add the form to change the visibility. So we are back in the form section.
Let's go all the way down to where we rendered the video player. And now what we're gonna do is below the video player outside of its div, we're gonna add a new div with a class name, EX4 and PY4. So basically just padding four, yeah. Flex, flex column and gap y of 6 now we're gonna go ahead and add another div so we are just doing proper positioning now So justify between item center and gap x2. And let's go ahead and add another div here with a class name flex flex column and gap y1 and finally a paragraph video link.
And let's give this a class name of text muted foreground and text extra small so now we should have a video link button I mean text here and then inside of here we're gonna add a div with a class name flex item center and gap x of 2 and this will use an actual link from next link so just make sure you've added an import for that here. There we go. So an actual link from next link with an href which will go to slash videos slash video dot id like this and inside a paragraph and for now we can just say you know HTTP localhost 3000 123. Let's give this a class name of line clamp 1, text Small and text blue 500. There we go.
So we can now see our mock URL link here. And what I want to do is I want to add a little button next to this link, which will have the copy icon from Lucid React. Make sure you have added this. And it's going to be a type of button. This is very important.
Otherwise, it's going to default to submitting. It's going to have a variant of ghost, a size of icon, a class name of shrink zero, onClicked will be an empty arrow function for now, and disabled will be false for now. There we go. You now have a nice little copy button here. And now let's actually enable it.
So this is what we're going to do now. Let's go before we return here and let's do const full URL is going to go ahead and open Vectix and we're gonna use either process.environment.versell URL or HTTP, whoops, HTTP localhost 3000. And then we're gonna go to slash videos and then video ID so that's the full URL I'm gonna add a little to do here change if deploying outside of Vercel so for this tutorial we will be deploying on Vercel so this will work but you know add a note here just to use a different environment variable, like next public app URL, which you're then gonna change in your environment variables later on. All right, and now let's go ahead and let's add a little state here. From use state from React, so is copied and set is copied.
And now let's go ahead and actually use them. So I'm going to create const on copy like this. And I will simply do a wait. Actually, let's just do navigator.clipboard, write text. And pass in full URL.
Let's make it asynchronous simply so we can do this. Whoops. This is actually a promise, so you can await it. Set is copied to be true. And then set timeout.
Set is copied to be false. And do that after two seconds. And now let's go back to our button here, add that on copy. If is copied, disabled it, and then render icons dynamically. If is copied, we can render a copy check icon, otherwise the normal copy icon.
Make sure you have added the copy check icon. So now when you click this, you will get confirmation that it's copied. There we go. And you can replace this hard-coded with actual full URL. There we go.
So now I can click copy here, and when I paste, you can see that it's copied. Great, so we have that feature. Now let's go ahead and let's actually develop the rest which is showing us the video status. So outside of this button, outside of this div, outside of this div and outside of this div right here we're gonna add a new div and just say, hello, test. There we go, so it should be, you know, underneath here.
And let's give this a class name, flex justify between items center. Another div inside with a class name of flex, flex column, gap y of one, a paragraph saying video status and a class name, text muted foreground and text extra small and then a paragraph here snake case to title video max status max status can be empty so let's default to preparing And let's give this a class name of TextSmall. Import the SnakeCase to title from our libutils and I will just move it here. So now you should see the video status here as well. And then if you want to, you can do the same thing.
So just copy this entire thing. And you can choose track status, right? Or subtitles status and just use track status here. Max track status, and you can default to no audio like this. So by default, it's gonna say no audio or no subtitles, maybe better.
Like this. So some other video, for example, will say no subtitles in case you made this video before you enabled the subtitle generation or before you actually, or if the video doesn't have any audio, right? So this will be our default state and then the webhook will simply update the status to ready. Great! So we've actually wrapped that part up.
What we can do now is we can go ahead and add the last element here. It's going to be easier for us to know where it goes if we go from the back. So where the form ends, where the inner form ends, where this div ends, where this div ends, and then just in between those two, right? So let's add another form field here. And what I'm gonna do is I'm just going to copy the last form field which we've created here for the category ID.
I'm just going to copy the entire thing because it will be similar. So let me just copy that and add it here. So now you should have category here as well. We're going to change this to control our visibility, like that. And we're going to change the label to be visibility as well.
Select visibility. And We're very simply not going to iterate over anything because we know that there are only two values here. The first value here will be public and we're just going to say public and the second one will be private and the value will be private and if you want to, you can add a globe2 icon from Lucid React with a class name size4 mr of 2 and in here, you can add a lock icon So just make sure you added all of this imports from Lucid React. So there we go, looks like it's broken. Okay, let's go down and let's just fix it.
So I believe that both of these need a div with a class name FlexItemsCenter. And we have to do the same thing here and just end the div. There we go. Perfect. So by default, all videos will be private and then you can change this to public and click save and it should work.
If I go back here, there we go. This one is public, This one has changed the description and the title. Perfect. And let's go ahead and try out our status if they work. So yeah, how would we, you know, click this and go inside?
You can see how it's going to look for a video that's obviously, you know, broken. So this is how it's going to look like. And you can try out what happens if you like remove this, if you do this. I'm not sure if we get an error. Yeah, so this happens.
So if you want to, you can fall back to this, if you think it's better than an empty space. Looks like there are no errors in the console. So I think it's okay to do that. Yeah, if you want, you can comment this out and instead fall back this because then it will always render the video player. It will just mean that the video is not actually something we can load.
:11 Right. Great. So this now works and let's just go ahead and add the delete functionality. And I also want to do one more thing, which is I want to go back to my studio upload model. We have an onSuccess, which we never actually use.
:34 So how about we go ahead and use it now so on success the following will happen on on success first things first we're gonna check if we don't have data video ID and we can simply break this method. Looks like it cannot find the data. How can it not find data? Oh I mean create.data video ID. My apologies, that's what I meant.
:10 In that case, let's go ahead and let's re-invalidate some things. So, studio, getMany. Actually, it's already doing that here, so no need to re-invalidate get many. But what we can do is... Actually, no, no need to do any of those.
:28 Yeah, let's just call reset here. Sorry, create.reset. And then let's do router, which we have to add. Use router from Next Navigation. Do go to slash dot push slash studio videos, create data video dot ID.
:56 Basically our newly created form route, right? Once the video has been uploaded, that's where we're going to redirect the user to. So let's try it out now. If I click create, I have a model, I will upload this video and I'm redirected here and it says waiting and no subtitles and if I refresh there we go now it says ready but still no subtitles but if I refresh again, subtitles are now ready. So we can see how webhooks are coming one after another.
:29 Perfect. So that works. I think we finished our form. Obviously, there are some more things to do. We have to enable thumbnail uploads and then we have to enable background jobs, which we are going to use to generate the title and the description of the video based on our subtitles.
:47 So those are only going to be available if we have subtitles. Great! Amazing, amazing job! And I think we can use this chapter just to wrap up the delete method because it's very easy. I don't want to leave it for something else.
:00 This is a long chapter. It's been over an hour already. But let's go ahead and go inside of modules let's go inside of videos UI a my apologies server procedures and we're just gonna add delete here again protected procedure Input and inside of here we actually only need one thing which will be the id z.string.uuid. Let's import z from Zod. Now let's add the query.
:36 It's going to be an asynchronous query. We can immediately destructure the context and the input. Let's go ahead and get the user, the ID user ID from context user so we know which user is trying to do this and then let's do const deleted yeah here's the thing I don't like using delete here simply because delete is a reserved keyword so I will be using remove. So this will be removed video await database delete videos where and then we will have to use our end and two equals inside. First one will check if the video's ID equals the input ID and the second one if the video's user ID equals the user ID which we have.
:32 And let's add returning here. If there is no removed video, that means that we can throw new TRPC error here with code not found. Otherwise we can return back the removed video. Simply so the API end user knows which video was removed. And we can do the same thing in the updated procedure, return back the updated video.
:02 There we go. Great. So we now have the remove procedure and it's protected to only allow us to remove a video which we own. So let's go inside of our form section. Let's go ahead and let's add it the same way we added updates.
:19 So I will just copy this. This will be called remove TRPC videos remove on success. No need to well, we can invalidate. Yeah, let's not invalidate this one. It's just going to say video removed.
:35 We are going to invalidate many and then we are going to redirect the user away from here. So let's get our router, Import this from Next Navigation. Like this, we have the router, and we're just going to do router.push, the user back to Studio. On error, something went wrong. Great.
:08 Use mutation. Oh, procedures. Did I do something wrong? Oh, I did dot query. It should be dot mutation.
:16 There we go. So now use mutation should have no error and now let's use this inside of our drop down here. Unclick, remove, remove.mutate And we have to pass in the video, the ID to be video ID. There we go. So now it should work just fine.
:40 If I go ahead and decide to delete this, there we go, video has been removed. And I should be able to delete this. There we go. Perfect. So I think we did a very good job in this chapter.
:54 Let's get our handy checkmark here. We added the skeleton, created the form page, created the video player, and added the ability to update the title, description, the category, and the visibility, and even more than that. Amazing, amazing job.