{
  "video_id": "ig26iRcMavQ",
  "title": "Build a YouTube Clone with Next.js 15: React, Tailwind, Drizzle, tRPC (Part 2/2)",
  "url": "https://www.youtube.com/watch?v=ig26iRcMavQ",
  "transcript": "Hey there, my name is Antonio and in this course I'm going to show you how to build a fully functional YouTube clone using modern web technologies. Our video player comes with all the essential interactions. Users can adjust volume, switch quality settings and even toggle captions. While watching, users can like videos to show their appreciation. They can also grow their feed by subscribing to channels they enjoy. The comment section below is where the community comes alive. Users can share their thoughts by leaving comments. These comments aren't just static. Users can like them to show support. And the conversation doesn't end there. Users can reply to comments, creating engaging discussions throughout the community. But what you've seen so far is just scratching the surface. Creators can access their studio, a dashboard where they manage all of their content. The video goes through multiple processing stages. Within moments, the status changes to Ready as our infrastructure, powered by MUX, generates video thumbnails, optimizes the video quality and creates transcriptions. We've built a versatile thumbnail management system where creators can upload custom thumbnails, AI generation or restore previous versions. But here's where it gets even more exciting. With just a click, creators can leverage AI to generate optimized titles and descriptions based on the video transcriptions. They can monitor these background processes in real-time through the technical dashboard. Once everything is set and AI has worked its magic, creators can review the generated content and publish their video with confidence. Besides the home feed, we are also going to have trending and subscribed feed, as well as custom user profiles where you can find specific videos from one channel. Another useful functionality we are going to develop are going to be playlists. Creating a new playlist is as simple as clicking the plus icon and once created, this playlist Playlists become powerful tools for content organization. Users can browse to their feed and add any video to their playlist using the convenient drop-down menu. This makes it easy to build collections of tutorials and organize content exactly how they want. Users have full control over their playlists, they can remove individual videos or delete entire playlists if they no longer need them. Besides all of that, the platform also maintains automatic playlists like watch history and liked videos. And of course, the entire application is going to be fully responsive. Users can enjoy the same seamless experience whether they're watching videos, managing playlists or even uploading content from their mobile devices. This is my most comprehensive course to date. spanning 38 chapters and over 24 hours of in-depth content. Throughout this journey, we'll dive deep into advanced concepts. You'll learn how to combine Next.js 15 and React 19 with trpc, mastering server-side data prefetching and leveraging suspense in client components. We'll implement a module-based file structure that scales, but where we'll really push the boundaries is database management. Using Postgres with Drizzle ORM, you'll master query building and advanced SQL concepts. You'll learn different types of joins, how to leverage common table expressions and subqueries, and most importantly, how to write powerful combined queries instead of chaining multiple database calls. This isn't just about a YouTube clone, it's about mastering modern web development. And now, without further ado, let's get started. If you wish to support my work, consider becoming a premium member on CodeWithAntonio.com and you will gain access to all current and future source code as well as a growing library of over 180 hours of content. So in this tutorial, I'm going to take a bit of a different approach as opposed to my other tutorials and I'm going to be starting each of my chapters with a whiteboard summary of what our goal is for the certain chapter. For example, this is chapter one, the setup. In this chapter, our goal is to configure the environment, which means choose a runtime, choose a package manager, establish some basic BUN commands, and create the basic Next.js project and wrap it up with some useful VS Code extensions. I got a lot of feedback from all of you that sometimes it's not exactly clear what or why we are doing something in my chapters. So I hope that this kind of approach will make it easier to understand and follow along. I will especially try and focus on some UI elements, which I've gotten feedback that it kind of seems a mystery to what it will be until it's finished. So I will be using this whiteboard at the beginning of each chapter to add some screenshots here so that you can see what our final build for this specific chapter will be. So this first chapter is quite simple. Our goal is to just get a project running. As I've already said, we first have to choose our runtime, which will either be Node.js or Bun. And we also have to choose our package manager, which can be npm, pnpm, yarn, or bun. As you've already noticed, I have highlighted bun in these two places. And in my little legend here, I've marked that as highly recommended. Why do I recommend bun for this tutorial? First of all, I recommend it because you will get the same environment as I do. Basically, I will be using bun in this tutorial, which means that I will be running bun commands. So if you want to follow along exactly as I do, it will be much easier for you to use Bun as well. The second reason is we're going to have to run some scripts, which will have TypeScript inside of them and some ES6 imports. Now, I am aware that in the newest versions of Node.js, we are capable of doing just this, but I'm certain that we can do it with Bun. I'm not 100% sure if Node.js can do it now. I've seen some updates around this, but I am certain that we can do it with BUN. So for example, we're going to add a little seed script to add some elements to our database, and we can easily run that script using BUN. Whereas with Node, at least in previous versions, we would have to add a proper type script configuration, and we would have to change ES6 imports to the old require methods, and all of those things just to run a simple script. So for that reason, again, bun gets a point for me. And the last reason is we will have less issues in regards to dependency issues when it comes to React 19. So if you are not aware, React 19 recently came out. And while that is great, not all packages that we will be using in this tutorial have upgraded their peer dependencies to use React 19. What happens then is that certain package managers will throw errors or warnings when you try to install a package inside of your React 19 project, which will be the case for this tutorial because we will be using React 19. For example, npm will straight up throw an error and prevent you from installing until you add a flag dash dash legacy peer depths or dash dash force. This is something that I did in my previous tutorial, Google Docs clone, and it was just a hassle to write every single time. Yes, you can store these flags inside of a npm rc file, but you can already see just how much we're complicating just to install a simple package. When it comes to Yarn, it will throw a warning, but it will install the package. But for Bun, it will simply work. You won't even notice that there are any dependency issues inside with React 19 and the package we are trying to install. Great. So if you want to use Bun, you will have to visit bun.sh or simply Google Bun Runtime. So yes, Bun is both a package manager and also a node-compatible runtime. So I'm going to, of course, show you everything I have installed in my system so you can follow along. But go ahead and visit the website if you don't have BUN. If you have Linux and macOS, you can use curl to install BUN. And if you use Windows, go inside of your PowerShell and go ahead and run this. There are additional ways you can install. If you go into docs here, I think maybe Windows users might have some questions, especially if they don't use PowerShell. you can install it using npm as well, also known as the last npm command you will ever need. So go ahead and run this command if you are on Windows and you don't use PowerShell. If you use PowerShell, you can just run this, right? On macOS and Linux, you can also do this. You can also use brew, you can use Docker, or you can simply use curl right here. Once you've done that, what you're going to do is just confirm that you have BUN installed. As you can see, this is my current BUN version. I'm also going to show you my Node version just in case. If you don't have Node, this is a bit of a conflicting advice I'm giving you now because I just told you we're going to use BUN, we're going to use BUN. I would recommend you have Node installed as well. I think majority of people watching this tutorial will have Node installed, right? But in case this is your first ever venture with JavaScript. You have chosen a pretty hard project to start with, but I will support you through this journey. Go ahead and install Node.js. I think that installation is even simpler or as simple as BUN. Just find the download latest long-term support. So confirm you have Node version. Confirm you have BUN version. You should not be getting errors for any of these. Next, go ahead and confirm you have NPM just in case, right? So when you install node you will also have NPM installed And for BUN well BUN is both the environment I mean the runtime and the actual package manager So this confirmed that we have that right Now let go ahead and let's see. So, I'm just going to add some ticks here, right? So, we selected our runtime here. Let me increase the opacity. There we go. We selected our package manager. We learned why we're going to be using bun. Now let's establish some basic bun commands. And for this, we can actually already start creating our Next.js project. So here I am on the documentation page of Next.js. And here we have some system requirements. So first of all, it says that we need to have Node.js 18.18 or later. So that's why I advise you to install Node.js just in case. I usually don't tell this in my tutorials because I assume a lot of people already have this installed. But just in case, it's never wrong to have Node.js installed. Just make sure whatever version you see here is not lower than whatever is written on the documentation right here. And you can see that the operating system supported are macOS, Windows, and Linux. And we're going to be using this command right here to install our Next.js project. But before you go ahead and run this, you will notice that it's using npx. So this is a Node.js specific or npm specific command. But I just told you we're going to be using bun in this tutorial. So I will show you an alternative. And we're also not going to be using this at latest. I will explain why in a second. So let's go ahead and learn this. Whenever you see npx in bun world, that's equal to bun x, right? And whenever you see npm install in bun world, that's equal to bun add and then name of the package. So we just learned this basic bun commands right here. So now let's say I want to see what is the latest version of this package, because we want to install an explicit version here and not just use at latest, because Next.js can introduce a lot of breaking changes in a very short period. So I don't want my tutorial to get outdated by the time you found it here. So this is what we're going to do. We're just going to copy this. And instead of using npx, we're going to be using BunX. And then we're just going to add create-next-app at latest. And then just dash-version. And there we go. We now have the version. So this is the version I will be using throughout this tutorial, which means in order to run this command, let's finally do it now. And px, I'm sorry, bun x create next app at 15.1.6 right here. So instead of latest, we'll be using this specific version. I'm going to call this project new tube like this. I'm going to be using TypeScript. I'm going to be using Esselent. Quick tip, you can select this answer using arrow keys, right? So yes for TypeScript. Yes for Esselent. Yes for Tailwind. Yes for Source Directory. Sometimes this will be defaulted as no. So be careful. Move it to yes and then press enter. Make sure you select the app router. This is absolutely crucial because we will be using and leveraging server components very much in this tutorial. I have not yet used Turbo Pack. I've heard great things about it in development mode, right? So this is TurboPack for development, which is supposed to make your dev environment much faster. I would recommend selecting no for this tutorial purely because I will not be using it, right? So I cannot guarantee you how different of a project you're going to have if you select yes. So keep it no for now. I will not be customizing my import alias. I like the at sign. If you don't know what an import alias is, I will show you that once we open up our project, but just select no for now. And there we go. Bun install will now be running and installing all of our dependencies. There we go. We now have installed our new project. As you can see, it's inside of my users. And then whatever my folder was where I ran this command right here, bun X create next app. And inside of here, we have new tube. So I can now go inside of change directory, new tube like this. And there we go. you can see some files here. And now we're of course going to open this up in Visual Studio code or whatever is your IDE of choice. I would highly recommend going with VS Code simply so you can have all the same extensions as I will. So I'm going to go ahead and click on open here and I will select new tube, which is my newly created folder here. If you get this prompt, you can safely press Yes, I trust the authors here. All right. So now let's take a quick peek at our structure here. So outside of, I mean, in the root of our project, we have all of this config files. We will later revisit the tailwind.config.ds. This one is actually quite important, right? So this configured tailwind and enabled tailwind in this specific folders, pages, components, and app, right? But later on in the source folder, we're going to add some new folders here, which will not be covered by Tailwind by default. So we're going to have to remember that and quickly go back here so we can learn how to enable that as well. But we're not going to concern ourselves with that right now. I just want to bring your attention that even though we will mostly be working inside of the source folder here, there are some useful things that we are going to tinker with throughout this project, right? Ensure that you have the Tailwind file. Ensure that you have the TS config. Basically, everything that you see here, you should have as well. And of course, we have our BUN log file because we opted to use BUN. Inside of your source folder, you should have the app folder, which is basically our router. So our app folder is a reserved folder in which there are certain reserved files like layout and page, which are used to create routes, right? So if you go ahead right now and do bun run dev or npm run dev, if you choose to use npm, you can already visit this at localhost 3000. So let's quickly go ahead here and visit this page. I'm just going to zoom in a bit and there we go. So I am currently past PM here. So my entire project is in dark mode. Your might be in a light mode. You might be seeing a white background here. Don't worry about that. We're going to default to one of those later. Great. So we now have our basic project here, but I don't want to end the chapter here purely because we added this, add Shazian UI. So we learned how to use the exact version. We got familiar with the structure and now we have to add Shazian UI and just take a peek at some useful VS Code extensions. So let's go ahead and let's go. And for now, I would recommend you shut down the app and instead go ahead and visit ui.shazian.com or simply Google Shazian UI. And inside of here, we're going to go inside of the docs here. So what is ShadCN UI? As you can see, it is not a component library. It's a collection of reusable components that we can copy and paste into our apps. And I honestly think this is my favorite way of building our own component library, right? So let's go ahead and head into installation. Let's select Next.js for the framework. And now we're going to go ahead and install this. And as you can see here, it will tell you something. If you're using Next.js 15, see the Next.js 15 plus React 19 guide. Let's click on that just to see what's going on. So you're going to see what's written here is exactly what I've told you in the beginning. If you are using NPM, you can install Shatsy and Unidependencies with a flag, right? So basically, that's what I wanted to avoid. The Shatsy and CLI will prompt you to select the flag when you run it. but there are no flags required for PNPM, BUN, or YARN, right? So what's happening? React 19 at the time they were writing this was in release candidate. Now it's no longer in release candidate. As far as I know, it's completely out, but this is still in progress, basically. All the other packages need to update their peer dependencies to include React 19, right? So if you use NPM, this will happen. You will get this kind of errors. And then you have to choose either using force or legacy peer depths. So that's why I was highly recommending to use bun because bun will not have any issues for this. Great. We can now go back to the installation. Sorry for this hijacking. I just wanted to explain once again. So let's go ahead and select bun here and let's copy this command. So bunux dash dash bun chat scene at latest init. But before we run this, this is what I will actually do. I will just copy ShatZN at latest and I will do Bunnix ShatZN at latest dash dash version. So I just want to see the version of the latest ShatZN and that is 2.1.8. Great. So now we can go ahead and run this command here. But instead of using at latest, we will use at 2.1.8. So the reason I want you to do that is so you don't have any surprises, right? These things can change very fast, and I want to ensure a certain level of longevity in my tutorials. So no matter when you found this, I want you to have the same experience. So for this style, you can select New York. For the color, as the base color, you can select neutral. You can select yes for CSS variables right here. And this should add some new components. components.json. It should modify Tailwind config and it should update this globals.css file and it should also create source lib utils. Let's go ahead and quickly check that out now. So as you can see, I have six modified files here and all of those are exactly what we've written. So we have the new components.json, which is basically a configuration file for chat.cn. We have modified package with all the necessary new files here We have modified tailwind And yes if you go to the bottom here you will notice that there is an error here I never had this error causing me any problems It did not cause problems with the build. So I will leave it as it is. I will not fix it or anything. So let's see what else has changed. Globals.css was modified, as you can see. And now what will happen, what you're going to notice now is that you should no longer have a dark or light mode. It should always be light mode. So if you go to one run dev right now, and if you revisit your project, just make sure you refresh it. There we go. Now it's in light mode. If yours was already in light mode, you probably didn't notice any changes. But for example, for me, I was in dark mode and now it is light mode. Great. So you have officially and successfully installed ShadCN as well. And now let's wrap up the chapter by learning some useful VS Code extensions here and just confirming whether, you know, Tailwind works and maybe learning how to add some components. So let's go ahead and do this. Let's go inside of source, inside of our app folder and inside of page.tsx. So what you see on this page is what's written inside of here. What I'm going to do is I'm going to remove everything from my return method. I'm going to remove the import and I will start from scratch with a little div here and a paragraph, hello world, like this. So you should just see a small text, hello world, written here. Now let's try out some tailwind. So I'm going to give this a class name, font bold, and text rows 500. Let's go ahead and try it out. There we go. Hello world, and text rows 500 like this. Now I'm going to go ahead and go inside of my extensions here, and just go ahead and look at Tailwind CSS IntelliSense. There we go. So I would highly recommend that you have this extension. I'm going to click install again like this, and I will reload my window. Looks like something's happening. Yeah, it didn't exactly load the first time, but now you can see that this extension, Tailwind CSS IntelliSense, which we have just installed, is very useful. First of all, you can already notice that it will tell you which color this is in case you are not familiar with its variables. But the second useful thing is that when you hover over something, you can see the exact CSS. So if you're not sure, if you type unknown style, you have no idea, is this from Tailwind or not? You can hover over it. And if nothing pops up, this is not Tailwind. This is not a valid class name. So that's how you know if you have typos, for example, text roses, you can see that this is obviously something wrong here because Tailwind is not telling you anything about it. And you also have some auto correct here like this. Great. So Tailwind is confirmed to be working. And now let's just wrap it up by installing the components we need from ShatCN. So in ShatCN UI here, you can install each component individually, right? So you only use those that you actually need. But it is actually much easier for us to install all components at once, simply because we're just going to do it now in this first chapter. and we're not going to think about it for the rest of our tutorial. So we don't have to remind ourselves, oh, what was the version that I was using, right? So this is what we're going to do then. I'm going to shut down my project. I'm going to do bunex-bunex-bunex-2.1.8-all. I believe this is the correct command. And I think that this will add all the necessary components that I will need. There we go. So you should now have all of these components added to your project. It says 50 files for me, at least in this version right here. It has also updated some config, also updated some globals here. So I'm going to do run dev again. I'm going to go inside of here and I will refresh my local host. And I'm going to try in the root of my page here to import a button, for example. Let's import button from components UI buttons. So this is an alias. This is what I was talking about. So instead of having to go dash dash components, right? Obviously this isn't all that horrible, but if we were deeply nested in some folder, we would have to do this, this, this, right? All the way to find it. But with an alias, we can just use this because alias defaults to our source folder, right? So whenever you add the at sign in your import, it will act as being in the source folder. So you can then easily go inside of these components, which was just created because of this command we run. And inside of here, you can see the source code to every single component. This is what I think is super cool with ShatCNUI because the source code is not obfuscated inside of node modules as it is with other component libraries, but instead you can make it your own. So I'm just going to go ahead and remove the paragraph from here and I'm going to render this button and say, click me like this. And there we go. We now have a button which says, click me right here. And I can go ahead and give it a variant of destructive, for example. And now you can see it has a different variant. I can also go directly inside of this button. So what I did was I pressed command and pressed inside. Or if you're on Windows, it's control and then go inside. Or you can go inside of source components UI and find the button. And inside of here, you can modify anything you want, right? For example, inside of here, we have the variants. So if I were to add a new variant and give it the background of purple 500 with a text of white, that's it. I can do that. So now I'm going to go here and I'm going to change this to variant new. And there we go. That's my new variant right here. That's how simple it is to build your own component library. And you can do the same thing for the paddings, the roundedness, anything you can imagine. The names of this, if you don't like default, destructive, and want to call it, you know, some kind of different semantic way, you can do that. Nothing is preventing you from doing that. And now you can remove this new style if you added it as I did. And you can see that it will immediately throw an error here like, hey, you can't do that. That does not exist. Great. So that was a quick intro. I believe we covered a lot about how we're going to be using all of this. And we just added some useful VS Code extensions here. Great. I think this is a great start. So what we're going to do in the next chapter is, well, you're going to see in the white bar, which will come. Great, great job. In this chapter, we're going to go ahead and build the basic layout for our app. As you can see on this screenshot, the basic layout will include our sidebar and our navbar right here. The full components will be the sidebar itself, sidebar sections, these and this, and then sidebar items like home subscriptions and trending and history, liked videos and all playlists. Then we're also going to develop a navbar with the search input and sign in component. So this is the search input and this is the sign in component. In order to do that, we're going to have to add this logo right here. And I have prepared that right here in this guest. So you can download it using the link below, or you can just use my source code and go inside of the public folder. You're going to learn about the public folder in a minute. So what we're going to do is let's start by adding this to our project, right? So I'm just going to go ahead and right click, save image and download this. And then what I'm going to do, or can I maybe just copy the image? Yeah, I think I can also maybe copy it. Just go inside of your public folder and let's try. I cannot paste it here. So I'm just going to go ahead and drag it inside of my public folder. There we go. I'm going to rename this to just the logo.svg. And there we go, right? So it looks like YouTube's logo, but it is not, right? I just made it in Photoshop. Great. So we now have this logo right here and we can already try it out on our homepage. So I'm just going to go ahead and do bun run dev and I'm going to refresh my page here to ensure that it is running. And I'm going to use the image from next image. Right. So we can remove the button import here and I'm going to give it a slash logo dot SVG with a height of 50 and a width of 50 and an alt of logo like this. So make sure logo.svg is exactly what you have in your public folder here, and you should be seeing the logo right here. Great. And then after that, let's just for fun add new tube text here. And as you can see, it doesn't exactly look like we are planning for it to look like, which is a bolder font and a different font here. So let's go ahead and learn how to change the font of our project. So for that, we're going to visit our source app folder layout.tsx. This is also known as the root layout. And inside of here, they already specify this Gaste font. So what we're going to do is we're going to remove this. We're not going to be using that font. And we're going to import inter from next font Google. So we're going to be using the inter font. And we're going to remove these two variables. and we're going to replace it with a single called enter. We're going to use the enter class and we're going to execute it. And let's give it subsets of Latin. So now we are using Google Fonts with Next.js integration and we have that font inside of this constant. So what we can do now is we can go inside of here and we can simply remove everything inside and just do enter the class name here like this. So now this will use the new font, right? So what we can do already is we can go ahead inside of our page.tsx and give this new to a class name of text extra large and font semi bold and tracking of tight like this And there we go Now it looks much better exactly as we going to want it to be right here Great So you learned how to add an asset to your public folder. You've learned how to use an asset. Just make sure you have added the height and the width. So this image component requires the height and the width. As you can see, it's going to give you an error if you don't use it. That's because the image component will automatically optimize images for you. You can, of course, also use the normal image tag if that's what you want. It's still going to work, but it's not going to be as optimized. So for this tutorial, I will mostly use the image. In some cases, I'm going to use the native image element, but most of the time, this is what we are going to use. Great. So we just added our logo asset. And now let's learn the basic app router folders, right? So I just want to check this. Let's get this out of the way. Great. Now let's learn basic app router folders. Okay. So for example, we are right now on localhost 3000. This is our root page. If I go instead of source app folder, and if I create a new folder called feed, for example, let's imagine that's how we're going to call our homepage, right? Feed. and if I create a new page.tsx inside, it needs to be called page.tsx and it needs to be inside of the app folder like this. And if I go ahead and simply export default page component and give it a div feed page like this, what's going to happen now, so what's crucial here is that you do export default. Please don't forget to do this, right? Save the file, make sure it's saved. And now go to localhost 3000 slash feed. And there we go. You can now see the feed page. So that's how you create routes inside of Next.js 15. But there are a couple of more things that you can do. For example, you can do a sub route. And while we learn how to do a sub route, let's also learn how to do a dynamic route. For example, let's do a video ID inside of square brackets like this. Keep in mind that I have used the capital letter I for ID. And inside of the same thing, page.tsx. I'm going to go ahead and do another default export here. I'm just going to call it page. It doesn't matter what the component it's called. And I'm going to call this feed or let's call it video ID page. So now, as you can see, this is not clearly defined what goes here. All that we know is that it's a subroute of feed. So if I go to slash feed slash one to three, it will load the video ID page. If I go to three to one, it will also load the video ID page. If I go ABC, it will do the same thing. So it's dynamic. It doesn't care what it's being passed. But here's what it's useful for. So inside of page.tsx, there is a way to extract those params and render it out here. So we can easily do it just by doing params like this. And then I can do video ID, params.video ID. Let's see if this will work. There we go. So you can see that it works. But in case it doesn't work for you, don't worry. It probably means that Next.js has officially deprecated this. I say deprecated because this is not how you should do it, right? It works and it's great because I can demonstrate how it works. So if I go to slash feed one, two, three, it will now show one, two, three, right? So this is basically a video ID. That's what it serves for. It's going to give us something to query the database with, right? But first of all, we have a problem. If you go ahead and visit your server or your terminal, you're going to see this. The route feed video ID used params.videoid. Params should be awaited before using its properties. And you can learn more here. So dynamic APIs are now asynchronous. This is a change that has happened in Next.js 15, which means that whenever you're using dynamic APIs, which are params, search params, cookies, draft mode, headers, any of those things, you need to await those things. So this is what we have right now. This works, right? Params.id. It's literally the exact thing we're doing, params.videoid. But it will issue a warning. It still works, but this is not how you should do it. So how you can fix it is this is their solution if you are fixing this in a large code base. So they added a code mod, which goes throughout your entire project and fixes it for you. We don't need that. What we need to do is learn how to properly parse this. So what we have to do is, well, first of all, let's go on interface, page props, params, video ID. Let's mark this as string. Let's add page props here. There we go. No more errors, right? But still, this doesn't fix our issue. I just added the types. What we have to do is we have to mark this entire component as an asynchronous component. And we have to give this a proper return type. So this will be a promise, which returns an object of video ID. And now we are getting the same error here that we are getting here. Now it makes more sense for us what we have to do. So that's why you should always know what you're expecting. So now let's do the following. Let's extract the video ID from await rams. And now let's use the video ID. There we go. What I'm going to do now is I'm going to scroll all the way to the bottom and just leave a couple of empty spaces here. I'm going to go to my local host and I'm going to refresh. Everything still works fine and this time no errors at all. So in order to use dynamic APIs like params, in this case, the video ID right here, you have to await them. If this didn't immediately click for you, don't worry because we will encounter this again and again. and it's going to become more clearer the more we work with this. One thing that you're probably asking is, how exactly can we async this, right? What exactly are we awaiting here, right? That may be confusing to you if you've never worked with Next.js before. Well, that's because this is actually a server component, right? And now I think you just have even more questions, but you can try and do this server component. If you go ahead and refresh this, you're going to find that console log right here in your terminal server component right so basically you can also you can also see that here in the inspect element but you can also see that it says server here whereas if you were to mark this as use client first of all you're going to get a bunch of warnings here that's because client components cannot be asynchronous components client components cannot do these dynamic API routes. They can do it, but they have to use a special hook for it, right? If you want to use client components, in Next.js, you have to mark your component as use client. And I think a better way of explaining it is to go to another page like feed right here, which doesn't use the dynamic API endpoint. I don't want to go too deep into this. I don't want to scare you off or confuse you. I promise you it is going to become very simple once we start to leverage the server components so you can actually see what they're useful for. I think that's the best way to learn, right? I can theorize about what server components are all day long, but until you actually see what they're used for, I think only then is when it's going to click for you. So don't give up yet if you find this confusing or unusual, right? If this is your first time with Next.js, you're probably wondering what the hell is a server component. So let's go back inside of feedpage.dsx and let's change our URL to go to the feed component. There we go. My apologies. This one. Feed page. There we go. And inside of here, I'm going to do the same thing. I'm going to do console.log. Where am I rendered? And when I refresh here, you can see that it's rendered on the server by default, right? So server components will be able to do things like const, you know, data, and you will literally be able to do database dot, I don't know, select from videos where equals videos ID one to three, right? And you would literally be able to render out data dot name in here, right? Imagine you want to do this. You would be able to do this in a server component. Server components act like an API, right? Imagine you're on the server. That's what a server component is, right? We're going to leverage this to prefetch our data. And then we're going to leverage the client component to enable the reactivity that we are used to, right? So I'm kind of introducing you into server components now. I promise I'm going to go deeper into this in the actual chapter dedicated for this. So let's go ahead and, well, of course, remove this and just write a feed page back. So what I want to show you before we actually go and build our layout is that if I add use client here and then refresh this, now you can see that it is first of all rendered twice and second of all, well, no server. And you cannot see it. Well, you can still see it here. Let me just add some space here and then refresh. Oh, it looks like you can still see it here. Okay. I did not expect that. I thought it was not going to be rendered when it's in use client here, but it looks like it is. Okay. But you can clearly see that it no longer says server. What can you do when it's in use client? Well, you can do things like use effect, right? Use effect is something that you would not be able to do in a client component. So I'm going to console log this inside of here. There we go. It works just fine. And now you can see that you cannot see it here in the terminal on the server because this use effect runs exclusively on the client, right? But if I remove use client, what's going to happen is you're going to get an error. You're importing a component that needs use effect. This React hook only works in a client component, right? So I hope this kind of gives you a very, very brief introduction into server components and client components. Client components, which in Next.js you have to always explicitly mark with use client, are your usual components that you're most likely used to. Whereas server components are the new type of components which can fetch the database and we can leverage them for pre-rendering, pre-fetching, basically making it.\nour app very, very fast. And once they actually click for you, you're going to like them. I promise you. At first, I was very confused about them. I didn't understand why they were the default component here. You probably noticed that, right? They are the default component instead of the app folder. Everything you add here is a server component. That's quite unusual. You probably are not used to that. But once you get a hang of it, it will prove to be quite useful. Great. So we've learned the basics of that. And now let's go ahead and learn about another. You can remove, actually keep the video ID and let's go inside of feed here and go ahead and add a layout.dsx file. So layout is another reserved file name. And you also have to do default export here. So layout instead of here, let's add layout. What's going to happen now is that just make sure you've added this inside of the feed folder. What's going to happen now is that if you go inside of slash feed, it's just going to render layout. If you go inside of slash feed one, two, three, it will also just render layout. That's quite unusual, isn't it? That's because layout functions work in a specific way. Let's create an interface to make this easier for us to understand. Layout props will always have children. Like this. Not node, it's React node. Like this. Let's assign layout props and let's destructure our children. And now let's render the children. Now everything is back to as it was. Feed123 or any other ID renders that. Slash feed renders the feed page. So what's the purpose of this layout file. The purpose of this layout file is exactly as it stands for, to create a reusable layout across components. If I were to go ahead and add a div here and say I am a navbar, like this, let's give it a class name of padding4, backgroundRows500, and the full width. You can see that now this appears on my feed page. And if I go to slash feed slash 123, it will also appear here. So this layout file is exactly what we need. That's why I wanted to explain it to you. Because this, the navbar and the sidebar, will be inside of a layout file. And what you're seeing right here, this part, this will be our page.tsx component. And this will be our layout component. Let me see. Can I draw on the picture? I can. Great. So this part will be our layout component, right? This part right here. All of this is our layout. And the rest is going to be our page component. So this will be reusable, right? No matter which page we are on, this part will appear the same. That's what we want. That's what I wanted you to learn the basics of this. And I think that we can now cross off this Learn Basic App Route Folders. Let's go ahead and mark that as Learned. Great. So this is what we are going to do. We're going to learn about one more type of folder. and then we're going to go ahead and build something. I promise. I know this went longer than you probably expected, but I think it's important to explain these things for you. I'm going to remove the entire feed folder now, and I'm going to show you one cool thing. I'm going to create a new folder, but inside of parentheses, and I'm going to call it home folder like this. Now, what I'm going to do is the following. I'm going to go ahead and remove this. Sorry, I'm going to close my terminal. Basically, I don't want my app to be running just because I don't want to confuse my hot reload. Then what I'm going to do is I'm going to copy page.tsx, right? So I recommend you copy it as well. You can also move it, but then you're going to cause some, well, let me show you. If you move it right here, click move, you're going to get this update imports for page.tsx. You can press yes for this if being asked. And then you're going to have this weird unsaved file coming from .next slash types slash app. So basically what's going on here? This is just cache. The cache is being confused. That's why I recommended that you shut down the app just so you don't have some funky errors that will confuse you but aren't actually real. If this happens to you, you will see one unsaved. Make sure it's the one in .next folder slash types slash app. You can click on it. you can go inside and save the file and close it. If you think you've done something wrong, don't worry at all. So ensure that you have moved the page.tsx, which was in the root of the app folder. You should now have it moved inside of home, which is written inside of parentheses. Very important. Make sure it's written inside of parentheses. And just for fun, let's remove the .next folder. Not any other folder, just .next. Remove it like this. I want you to learn to not be afraid of that weird folder because that folder is just cache. If you do bun run dev again, let's go ahead. And there we go. It's back right here. It just rebuilt the entire thing. So .next folder can sometimes get confused when you move around pages and layouts, right? If that happens, if you think you've messed up, just remove the .next folder. It will regenerate. Don't worry about it. So now our slash feed does not exist. So let's go back to localhost 3000 and you should still see the page file, even though it's inside of the home folder, right? If I change in YouTube too, it's going to change here in the root of my application. How is that possible? It's inside of a folder. That's because Next.js has special rules for how you name your folders. So the trick is not in the home keyword. No, the trick is in the parentheses. When you put your folder inside of parentheses, it tells Next.js router that this part, this specific part, is not going to be a part of the URL. It will still be a part of the router, but not a part of the URL. This is useful when you want to categorize or structure something in the same folder, but don't want to make an entire new URL route out of it. Because if you just added home, right? And then another page.tsx inside like this. You don't have to write this. I'm just doing it for demonstration. This one is only accessible when you literally go to slash home. But if you want to structure your root page and your root layout inside of a specific folder, you can do that with the route group folder. This is what that is called, a route group. And you can, of course, learn more about that here. Route groups right here. So route groups are nested folders. So in the app directory, nested folders are normally mapped to the URL paths. That's what we just learned with our feed, right? However, you can mark a folder as a route group to prevent the folder from being included in the URL path. They're useful for organizing routes into group, like site section, intent, or team, right? In our case, home is a site section. It's a home feed, right? And the cool thing is you can still do the layout file inside of it. So if I go ahead and create layout.tsx, it's only going to refer to my routes inside of this route group. That's why I wanted to put it here. Keep in mind that this layout is a different thing. This is the root layout. You cannot move that around. This one includes the HTML and the body. You can see that it's different than the one we've created because this is a completely different one. You will never move this one. You will modify it. You can, of course, modify it however you want, but you should not move this file. This is the root layout. But you can create another layout inside of here. And this is where we are finally going to go ahead and create our home layout. So, let's go ahead and let's create an interface, layout props, give it a children react, react node. Let's go ahead and create a const layout here. Let's assign the props and destructure the children. Inside of here, let's just render the children and let's export default layout. There we go. Nothing should change right now. If I go to localhost 3000, I should still see newTube2 from this page right here. So what we're going to do now is we're going to build our home layout. This is the decision that I will make now. I'm not going to do any components inside of the app folder which are not related to routing. So the only files that I will try to keep inside of the app folder will be routing files. Let me show you what I mean. I'm now going to create another folder inside of my source folder, which I'm going to call modules. Like this. And inside of my modules, I'm going to go ahead and create another folder called home. And inside of here, another folder called UI. You already see what I'm doing. I'm going to have a modules folder where I'm going to have my sections, my intents, my teams, basically whatever I need. And I'm going to use these modules to store my components, my hooks, my API, my server, my utils, anything I need. And then I'm going to use it as I need inside of the router here. But the router itself is not going to have this. I'm not going to have any styles inside of page.dsx. I'm not going to have any styles inside of layout. The layout will simply serve to render the children. So this is what we're going to do now. Inside of our modules home UI, we're going to go ahead and create a new folder called layouts. And inside of here, we're going to create a home-layout.tsx. Let's go ahead and let's copy the interface from here. We can actually copy the entire thing like this. The only difference is that our components, like home layout, are going to use named exports like this. This needs to be a default export because Next.js expects it to be a default export. But for our own modules and components, we don't have to do that. And I prefer this one. Let's go ahead and be a bit more specific with our name here. Instead of the generic layout, this will be a home layout like this. But now we're going to run into a bit of an issue here. I going to go ahead and attempt to do what I just did previously I going to create some kind of a nav bar right Home navbar And if I go ahead and refresh this my apologies not yet I have to go inside of my home route group layout here, and I have to import home layout from modules, home UI layouts, home layout. and I'll replace this with Home Layout like this. You can see that it says Home Navbar. Now I'm going to attempt to show you something, but I'm not exactly sure if it will fail like I expected to or not. So I'm going to give this a padding of 4, a background of blue 500, and I don't know, let's keep it like that. And there we go. You can see that something is obviously wrong. My padding works, but this doesn't work. How come it doesn't work? Well, first of all, how come this works? The secret is in Tailwind's Just-In-Time compiler. The padding-4 has already been used. So it was already, let's say, compiled. I'm not sure what's the correct expression to use. My apologies, but it's already bundled in what styles Tailwind currently has for our project. So when it sees padding4 here, it says, cool, I know what padding4 is. But we have never used BGBlue before. At least I haven't, right? So it's confused. It has no idea what that is. Why doesn't Tailwind know what this color is? Well, let me remind you why. What is our folder name? Our folder name is modules, right? Inside of source right here. Let's revisit our tailwind.config.ts. Can we see modules anywhere in here? We cannot. So tailwind does not work inside of our modules folder. That's why it's crucial that we now do a proper fix for that. So what we're going to do is we can just copy the last line, which is in my case, source app. and change this to simply be source modules. And now every single component inside of modules will have tailwind as well. There we go. We just added tailwind. I still have this little error at the bottom. Don't worry about it. It will not affect anything. Now let's go ahead back inside of our home layout. So that is inside of modules, layouts, home layout right here. And instead of using div as our main wrapper, we're going to use sidebar provider from components UI sidebar. We have this because remember a few, I mean a chapter ago, we run bunX dash dash bun shat cn 2.1 point add point 8 add dash dash all. And that has added source components UI sidebar right here. In case you encounter a component that I have, but you don't, double check that you can visit it inside of source components UI sidebar. If it really does not exist here, you can always add it manually. I would recommend, again, running the command that we just used previously, which was this, but you can also just add for sidebar. You can also just add sidebar, right? In case that happens, it should not happen to you. Great. So make sure you have encapsulated your home layout with the sidebar provider. Now for this div right here, we're going to give it a class name of full width like this. And then inside of here, we're actually going to render our home navbar component. Let's move the children inside of this div and let's go ahead and add encapsulate our children in a div here. So I'm just adding some semantics here. We are going to populate this with some classes in a moment. Right now, if you visit our app, home navbar is not defined as the error we are seeing. Let's go ahead and build the home navbar. So we're going to do that inside of the modules home UI, and let's open a new folder called components. And then let's open another folder called home navbar. Inside of home navbar, let's add an index.tsx file, and I'm going to export const home navbar, and I will return a div home navbar. The reason I have opened a folder for this and an index.tsx and not just home navbar.tsx is because it's going to have some subcomponents, right? So I just want to keep them all together. That's the only reason why. If this was a simple component, I would just, you know, write the name of the component dot TSX in case you were wondering. Great. Now let's import the home navbar from dot dot components home navbar. If you want to, you know, you can use alias, but then remember you have to go to modules, home, UI, right? But since we are already inside of here, I think it's just nicer to just use this, right? So now you should see home navbar again. There we go. Now let's go ahead and let's properly style this home navbar. Instead of using a div, it's going to use a nav element. Let's give it a class name of fixed, top zero, left zero. So it has a fixed position. Right will also be zero. White will be 16. Back one color will be white. We're going to add flex, item center. Padding on left and right will be 2, which comes to 8 pixels. Padding on the right will be 5 because we need a bit more padding on that side because of our login button. And the Z index will be 50, so it's above all other content. So right now, not much should really change except that your content will be hidden, right? So my navbar right now is above my content, so it's actually hidden. Basically, what we did here is we just created some basic rules for positioning our navbar, and we added some flex items here so that we can properly use flex to align this on this side, this in the middle, and this here, right? So that's why we added those flex elements. Great. Let's go ahead and continue developing. Inside of here, I'm going to add a div with a class name, flexItemsCenter, gap4. So between each item that I'm going to add inside of this div, it's going to be a gap of four or 16 pixels and a full width. Now, inside of here, we're going to render menu and logo. The menu will be sidebar trigger from components UI sidebar like this, like that. And let's actually encapsulate this in another div because we will have two items here like this. and let's give the div a class name flex items center and flex shrink zero so this never shrinks regardless of our resolution and now we're going to add a link component from next link which is basically going to be a click on our logo so let's give this an href to go back to the root page like this and inside we're going to do what we did previously which is render our logo dot SVG using our next image like this. Let's give it an alt of logo. Let's give it a width of 32 pixels and the height of 32 pixels as well. And now let's add a paragraph new tube. You should already be seeing. So we have this little button here and we have this new tube here. So what we have to do now is the following. We have to wrap this inside of a div like this so that we can fix the current state, which is that they are one beneath another. So why I'm going to add this div is a class name, padding4, flexItemCenter, and gap1. There we go. Now they're nice next to each other like this. And now let's go ahead and give this new tool a class name, text extra large font semi bold and tracking tight like this. There we go. Now it looks much better, right? It looks way more similar to what we have right here. Great. One issue that you can probably see here is this little icon, which is obviously different than from what I have here. We're going to fix that in a moment. I want to continue, you know, developing this so that we can actually see what this button is doing. And then I'm going to show you how to change what this button looks like. Great. So what we have to do now is we have to add our search bar, right? So find where this div ends, right? This div, which is our menu and logo. And now what we're going to add is our search bar. Okay, let me just add it here. So we're going to have a div here with a class name, flex1, flex justify center, maximum width of 700 pixels. So it doesn't expand more than that. And mx auto. So basically, I want it to be pushed equally from this side and this side, so it's going to be rendered here. and then inside of here let's add search input like this now we have to build the search input component we're going to do that inside of our components right here and we can actually put it inside of home number because that's the only place it's going to be used in so let's add the search input dot dsx like this so i'm going to go ahead and do export const search input here For now, this will not have any functionality, just the UI. So what I'm going to do is I'm going to add a form here with a class name, flex full width, maximum width of 600 pixels. I'm going to add a div with a class name, relative with a full width. And then I'm going to use the native input. input. So don't use the one from Shatzi and UI, just the native one. Give this a type of text, a placeholder of search. And let's give it a class name. And before we start writing the class name, let's actually import the search input from dot slash search input so that you can actually see what we're building. Right now, you should just see a random input here. So now let's go ahead and let make it pretty by giving it full width padding left of four So we leave some space for the icon right Because we are going to have the search icon here appear later Like this, PL4, PY2, PR12, rounded L, full, border, focus, outline none, focus, border, blue, 500. Like this. There we go. you can see that now it already looks pretty nice. And you can see a clear cutoff here. That's because we're going to add a submit button in this corner right here. So let's go ahead and let's do that. Outside of this div, we're going to add a button. We're going to give this a type of submit because it's inside of a form. And we're going to give this button a class name of PX5PY2.5, background color of gray 100, border. Border on the left side is gonna be zero because it will directly connect to here, as you can see. So we don't want to have double borders on this side. That's why it's zero on that side. Rounded on the right side will be full, right? So we want it to be fully rounded. So it matches this part like that. And then on hover, let's go ahead and give it some background gray 200 it. When it's disabled, in case it's disabled, opacity will be 50 and disabled will also say cursor not allowed like this. And inside of here, we're going to use the search icon from Lucid React. There we go. Make sure you have added this import. If you're wondering where does this package come from? It comes from ShadCN, right? In case you don't have it, you can do bun add Lucid React or npm install lucid react and here's my version exact but you will most certainly have this installed and let's give this a class name of size five there we go perfect so we have our search component right here but we are missing a couple of things and we're not going to develop them now instead, I'm just going to add to-dos. So, to-do, add remove search button like this, and to-do, add search functionality. For now, we are only worried about the UI. Great. Now, let's go back inside of our home navbar index here, and what we're going to do now is the auth button. So, So let's add a div here and a class name flex shrink zero items center flex and gap four. And let's render the out button component like this. Now I'm going to keep the out button inside of a new module called out. So let's create a new module like this. Let's create the UI folder. And let's create the components folder like this. And inside, we're going to have the out button dot dsx like this. Let's export const out button here. And now what we're going to do is very simply, we're going to add a button from components UI button, meaning it's from Shatzian. We're going to render user circle icon from Lucid React and a text sign in like this. I will just align my imports like this. And let's import this out button from modules out UI components out button. So the reason I put it there is because it makes more sense semantically for it to be there, right? We are going to reuse that. We will be able to reuse that component in other places. And I think it just makes more sense that it belongs to the out module rather than it belonging to the home module. You can make, of course, a different decision for yourself if you want to. Great. So we have a button here, but you probably noticed that it doesn't exactly look the same. So let's just quickly go ahead and resolve that. I'm going to go inside of the out button here. And we're going to go ahead and first of all, give our button a variant of outline. Then we're going to give our class name PX4, PY2, text small, font medium, text blue 600, on hover text blue 500. So just a slight change. Border blue 500 slash 20, rounded full, shadow none. And what we're going to do is we're going to modify the size of this SVG right here. You're probably thinking, can't I just do this? You cannot, unfortunately. That's because if you go deep inside of the button component here, you will notice that it actually changes the SVG size like this, right? So if you want to change this to size five, right? What you have to do is you have to use that same target decorator, SVG, and then size five, for example. if you want to make it bigger. But I think size four is actually okay. So you can just leave it like this. There we go. Now our button is looking much, much better. Great. So that wraps up the UI for our nav bar. And now we have to do the same thing for the sidebar. I will just add to do, add different out states, right? Because we currently, well, this out button is not really useful at the moment. It's not really doing anything, but that will change. So we are now pretty much wrapped up with the UI when it comes to our home navbar right here. And what we can focus on now is go back inside of the home layout here, and we should actually find a place to render our home sidebar. Let's go ahead and give this div, which is encapsulating our children, a class name, flex minimum height of screen and the padding top of four run just like this. So now as you can see we have moved our body which was hidden behind the note bar. It's now being pushed right here. Great. So now let's also render the home sidebar here and encapsulate our children inside of the main element. And let's give the main element a class name of flex1 and overflow y auto like this. And now let's focus on developing the home sidebar. So basically, this is just some semantics to make this content behave properly when it's encapsulated in between the navbar and the sidebar, right? Those are this flexes and overfly Y autos and all of those things right here. And the reason we are doing padding top for RAM is because that translates to 64 pixels. And if you go inside of home navbar and hover over here, the height of our navbar is 64 pixels. So what we're doing is we are pushing the entire content below the home navbar for the height of that navbar. So that's why our content is now visible, right? Previously, it was hidden behind. Let's go ahead inside of our components and let's create a new folder, home sidebar. And inside of home sidebar, let's create an index.tsx component like this. Inside of here, let's export const home sidebar. and let's return a sidebar components from components UI sidebar. Make sure you don't accidentally import the icon from Lucid React. Let's give the sidebar a class name of padding top 16, z index of 40, and border none like this. And let's add a sidebar content here from components UI sidebar. And let's give it a class name of background. So BG background will simply default to whatever our background color is, which in light mode is white. And now instead of here, we are going to add some sections, but I think that we should already perhaps be seeing the sidebar. So let's go ahead and import the home sidebar from components home sidebar the same way we have imported the nut bar. We can't see anything, but we can clearly see that something has pushed our content right here. And you can see that also when I click on this icon, I can collapse our sidebar, right? So before we do any changes to any of this, what I want to do is I actually want to go inside of the home sidebar and I want to add my first section, which will be the main section like this. And the main section will be another component which we're going to implement. Inside of the home sidebar here, create a new file, main-section.tsx like this. And this will most certainly be a client component because we're going to be using some hooks. So we need to turn this into a client component. And now let's go ahead and let's prepare const items here. It's going to be an array of items. The first item will be a title which says home. It's going to have a URL of a forward slash and an icon of home icon from Lucid React, like this. And that's how we're going to build our items here. The next one will be subscriptions, which will lead to slash subscriptions, actually slash feed slash subscriptions, like this. with an icon of play square icon, again, from Lucid React. So that's going to be our second one. And we're also going to add a useful little work here, alf true. We're going to use this later. It's going to be useful for us. Basically, this page will only be rendered to authorized users because this will be used to load videos from your subscriptions. If you're not logged in, you don't have any subscriptions, right? And this one will be trending. So feed trending. And this will have flame icon also from Lucid React. And auth is not going to be required for this one. Great. So now let's export const main section here. Let's return sidebar group from components UI sidebar. Sidebar group content. and sidebar menu. So make sure you have all of this from sidebar, sidebar group, sidebar group content and menu But while we are here we can also add sidebar menu button and we can add the sidebar menu item like this So make sure you have all of this Let me just collapse them so you can see them nicely There we go. Instead of the sidebar menu right here, what we're going to do is we're going to iterate over our items. Item.map, get the individual item, and then inside of here, render the sidebar menu item with a key of item.title because each title is unique, so we can safely use it as a key. And let's render the sidebar menu button component like this. Let's give this a tooltip of item.title. Let's give it an as child. Let's go ahead and give it an is active to be explicitly false. I'm going to change this in a moment. And on click for now will just be an empty arrow function. And inside of here, we're going to use our link from next link like this with an href going to item.url. And then inside, we're going to render item.icon like this and a span item.title. Let's give this some styles. So this will have a class name of flex items. My apologies, flex, then items.center. gap-4, and this span here will have a class name of text small, like this. So that's our main section. Let's go ahead and import our main section from .slash main section. And now you should be able to see home, subscriptions, and trending right here, which are all from our items, as you can see here. Great. So now what I'm going to do is the following. I'm going to go ahead and add a little to-do, change to look at current path name, to-do, do something on click, right? So these are all the things that we are going to have to do later, but for now we are just focused on the UI. Great, now that we have the main section, we can easily build the other section which we need, which is the personal section, history, liked videos, and all playlists, right? So what I'm going to do is I'm going to go inside of my home sidebar, and below the main section, I'm going to add a separator component from components UI separator. Make sure you didn't accidentally import this from a Redix, because you're not going to have any chatsy and styles. Let's give this separator, well, nothing. It should just look like this. It should just be fine. Great. And then below that, we're going to have another section like this. Let's go ahead and let's copy the main section and paste it here. And let's rename it to personal section like this, meaning this will be the currently logged in user's personal items. But we are still going to render them regardless if we are logged in or not, purely because the sidebar really looks empty when you're not logged in. So we at least want to show the user what they could have once they're logged in. So that's my decision to show that. Let's modify our, well, first let's rename this to personal section like this. And let's go ahead and inside of sidebar group, add sidebar group label. You have to import this and call it you. So sidebar group label is a new import here. Make sure you're doing this instead of the new copied personal section. Make sure you didn't alter the main section. Great. Now we're going to change the routes. So the title of this one will be the history, which will lead to slash playlists history, playlists history. And it will use history icon from Lucid React. And we're going to add out true. Next one will be liked videos. And this one will go to playlists liked. Oops. And it's going to use the icon thumbs up icon from Lucid React. And the last one will be all playlists. And this will just go to slash playlists. And it's going to use list video icon from Lucid React with alpha true as well. So make sure you have added history icon, list video icon, and thumbs up icon. So now if you save this and go back instead of your home sidebar and replace this duplicated main section with personal section like this, you should now see another section right here. Great. And we can now go ahead and go inside of source app folder. My apologies. Inside of, well, yes, inside of home page.tsx and just change this to, you know, I will load videos in the future like this and remove the import. And there we go. Now it makes more sense what this will be. So this is our sidebar with home. If you click on any of this, it will lead to 404 because we don't have any of those routes implemented yet. But you can see our basic layout is definitely coming together. Let's go ahead and wrap it up by just modifying the icon of this sidebar right here. You can do that by literally going inside of the sidebar component, which we can do because we're using ShadCN UI. So just go to source components UI and select the sidebar component. Let me just find it, sidebar right here. And you can see that we are using panel left from Lucid React. Let's add menu icon from Lucid React. And we can remove the panel left. And now we can just look for the error. So just scroll down and you will see an error right here, I believe. There we go. The component is called sidebar trigger. And what you can do is just change this to be menu icon like this. There we go. And now it's exactly the same as mine is. And another thing that you can do here is you can go inside of app folder. my apologies, inside of your modules, home, layouts, actually home sidebar index, and you can add collapsed, I believe. Is it collapsed? Just a second. I'm going to, oh, it is collapsible, I believe. Not on the sidebar content, on the sidebar, collapsible. And inside of here, you can choose icon, non or off canvas. And if you choose icon, you're going to enable the collapsed mode like this. I think this is quite useful if you don't want to entirely hide the sidebar, but you do want to expand your user's view, right? Perfect. So we now have this. And I believe that already if you go inside of mobile mode, let me go ahead and go here, for example. There we go. You can see that it also works in mobile mode. It does throw some errors. these are actually not our fault because this component that's doing this has been built from, you know, we didn't build the code for this. And this error that's being thrown comes from a newer version of Radix, which I guess the creators of ShatZN didn't exactly account for at the time. So now it's throwing an error because the way they implemented this sheet component or this dialogue component is throwing accessibility errors, right? But I think it's okay for a tutorial. I will research a bit before I completely end the tutorial if that has been resolved somehow, but I think in this version, it's going to stay like that. But those errors are not visible in production as far as I know. Great. So one more thing I can show you how to do is the animation. If you like the animation, great. I personally don't like the animation. I think it appears the app looks slower. So again, let's go back inside of our components UI sidebar. I'm going to show you where you can change or remove the animation. So basically, if you want to remove the animation, I don't think there's a prop for it, but you can search for any of the transition classes, right? And if there are any transition classes, you can remove them. So for example, I'm going to remove transition all from here. You didn't have to do this, but if you dislike transitions as much as I do, you can just remove them. So I'm going to remove the transitions here. Transition transform, for example. I'll remove that. Transition with height and padding. I will remove that as well. Transition transform. Remove that as well. And transition with. Let's see. Anything else? we do have this as well. And I believe no more transitions. So now let's see if I refresh. Oh, it still shows some transitions. I think I also have to change the duration. So I will remove all durations as well. Let me see. Perhaps that's what was the problem. Again, you don't have to do this. I just really dislike them for some reason. There we go. Maybe I could have just removed durations, but this makes it the same way it works on actual YouTube. So depending on what you prefer, you can either, you know, you can bring it back or you can change it as I did. Again, you can just search for, you know, duration or you can search for transition like this. and then you can just remove the necessary classes. You don't have to do it if you like the animation. And if you think you've messed it up, what you can do is, well, you can undo the file. But remember, you can always run Banex add sidebar. And what this will do is, let me just go ahead and save my sidebar component just in case I'm going to copy the entire thing. You can do this, for example. And let's see. Oh, yeah. It will basically ask you the file sidebar.tsx already exists. Would you like to overwrite? And you can just press yes. And then it's going to overwrite it and it's going to bring it back to normal. So I'm just going to remove. No, no, no, no, no. The entire thing. No. Okay. Great. So let me just double check that nothing important was modified for me. No. Great. There we go. So as I was saying, we just finished the basic layout. Great. So we built this red thing right here. It's not exactly finished finish, it's just UI, but it is the layout that we need. Great, amazing.\nAmazing job. So this chapter is going to be a bit smaller than our last chapter, which went on for an hour. In this chapter, our goal is to add authentication to our project. We're going to achieve that using Clerk. The authentication which we develop in this chapter is not exactly going to stop here. but in order to implement the things that I have planned for authentication in the future, we're missing a couple of modules like our database and TRPC, which is something we're going to add later. So I decided to use this chapter simply to add basic authentication, which means in this chapter, we have to integrate clerk. We have to add a sign in and sign up screens. We have to add user button, right? Which is this user button when someone is logged in. We have to add this kind of login screen. We need to add a middleware, which will protect our routes. And then we can finally use that out state. If you remember personal section, we have this out true, right? So we can finally use that to show a login model if an unauthorized user clicks on that. And of course, we have to learn how to protect routes using the middleware. Let's start all of that by going to clerk.com. Go ahead and create an account. And once you create an account, you should be able to visit your dashboard. I'm going to go ahead and create an application. I'm going to call it NewTube like this. And you can see it says sign in to NewTube. And now I'm only going to select Google as the sign-in provider. and I'm going to leave email, but you can choose whatever you prefer. Yeah, I would actually, yeah, I would like it to be Google. The reason I want it to be Google is because we're going to rely on users' first and last name to store in our database later on. And if we just use email, I'm not sure we can extract the first and last name from that. But if we use Google, Google should automatically have the user's first and last name. So let's just select Google sign in. Later on, you know, in your application, if you want to, you can decide to enable some more providers. But, you know, if you want to follow the tutorial exactly as I do, go ahead and just select Google like this. There we go. Perfect. So now let's go ahead and let's add clerk next JS. Instead of using NPM, we're going to be using bun. Let's do bun add. But before we do that, yes, I want to be specific with my versions. So I will do the following. Let's see. I'm not sure how to check the latest version. So what I will do is just do NPM clerk next JS. And let's see what is the latest version. So it's 6.10.3. Let's go ahead and do that. bun add at clerk slash nextjs at 6.10.3. Add clerk slash nextjs at 6.10.3. There we go. That should add clerk to our project. And now you should copy the environment variables right here. So let's go ahead inside of our project. and we have to create a new file, .environment.local, like this. And it should automatically be hidden from Git. So instead of your Git ignore, you should have .environment and then an asterisk, which means basically all environment files are ignored. And let's just add these two keys here. You should never share your keys with anyone. For the sake of this tutorial, I'm using a demo account with no credit card connected or anything. So I'm not at any danger of anyone stealing this, but you should not share it with anyone. You might forget that you added a card and someone can use these keys for you. So make sure you only keep them safe for yourself. So you should have the next public clerk publishable key and clerk secret key like this. Now let's go ahead and let's add the middleware here. So update your middleware file or create one at the root of your project or inside of the source directory if you're using the source directory structure. So we are using a source. So let's go inside of source and let's create middleware.ts. And let's paste this inside. If you're wondering what this regex is, you can see basically it says here, this ensures that it doesn't match the next JS internals and all static files. Basically, we don't want the middleware to run on loading our assets, right? But we do want it to run for API routes. And it's very cool that they added TRPC here because we will be using TRPC. In case your matcher looks different, which is possible if you're watching this into the future and their documentation has changed, I mean, I hope you used this version, Clark Nexerius 6.10.3. You know, you can use any version you want. I'm not telling you that you have to use 6.10.3, right? The same way I'm not telling you that you have to use the exact versions that I used for ShadCN and all these other things. But I want to ensure a certain longevity to my tutorial. So that's why I'm telling you the exact versions to install. So if your matcher looks different, if you're watching this into the future, I don't know, a year from now and maybe something changed, you can go ahead through documentation and try and figure out if something changed on your own. or you can just look at my source code and copy this from here. But more or less, this is very simple. This shouldn't be anything special. So now what we have to do is we have to add the clerk provider to our app. So let's go ahead and go inside of our root layout. So instead of source app layout right here, and we have to import clerk. Is it clerk provider? It is. It's clerk provider from ad clerk next.js. like this. And I'm just going to wrap my entire app inside of that. Clerk provider, just like this. And let me just indent this inside. There we go, like that. And now, this is what we're going to do. We can do bond run dev, so we can start seeing some changes. I don't think anything will really change yet, but let's go ahead and do the following. What I want to do now is I want to create my login screens, right? Usually in the past, that was thought how to do here initially, but they decided that it's simpler to just show you this part because some people don't want login screens, but I believe they still have those. There we go. Next steps is to utilize your own pages for authentication, right? Let's click continue to the next JS guide and let's see what they have in store for us here. So build a sign in or up page. That's great. I did not know that they have, uh, okay. Yeah. Let's go ahead and do this. So I'm going to go ahead and do app sign in and then sign in page.vsx. Let's do that. Let's go inside of source app folder. And let's add, how did they do it? So sign in. What we can do here is we can add them both instead of a route group called out, right? So they don't just randomly appear. So we know that they're here, but out is not going to be a part of the URL. So we can utilize that. So sign in, right? And then we have to create another catch all route. So this is a dynamic type of route, which will basically catch every single variable that's passed inside anything dynamic. So this can be literally anything. And I guess that's useful for them because they might want to redirect you to somewhere after you sign in, right? So that's what they want this type of catch all route. And then they want page.tsx inside and they want us to add the snippet inside. There we go. So now I believe that you should already be able to visit this page, but let's read what they have to say. Make the sign in our up page public. By default, clerk middleware makes all the routes public. This step is specifically for applications that have configured the clerk middleware to make all routes protected. Okay, so we don't have to do this, right? Because our middleware does not protect any routes at the moment. All routes are public by default. So this step is for applications that have configured the clerk middleware to make the routes protected. We don't have to do that, right? Because we're only going to have a few authorized routes. You know, YouTube as an application is mostly an open app. So I think that's completely fine. Okay. So now we should be able to go to slash localhost 3000 sign in. And let's see. So sign in is not available for me. How come? Auth sign dash in page dot dsx. Let's see. Why is this page not available? it's not available because i think there's a bug here inside of how i wrote this so i opened two parentheses but i only closed one there we go now it's proper syntax so now if i refresh there we go we can see a nice login page here so what we're going to do here is we can copy this entire thing and we can call this sign up like this, and we can rename this to sign up as well. And instead of the sign up, we can import sign up. There we go. Like this. And then instead of out, what we can do is we can add a layout.tsx like this. And this layout can serve as a centering layout, right? So let's do this. Let's do layout. And what we're going to do here is just add an interface, layout props. And we're going to call this children, react, react node. Let's add layout props. Children. And let's just render the children like this. let me just fix my typo and now nothing should change but you should be able to visit both sign in and sign up so this one will say create your account and this one will say sign in welcome back right so the goal of this layout is to create a reusable layout which will be able to center this So items center and justify center like this Let see Minimum height of screen with two E And there we go. Now it's centered. All right. This already looks better. But we're going to be using these screens as fallbacks. Most of the time, we will not show this kind of screen to the user. This will only show if the user manually tries to use the URL to go to an authorized screen. And then we're just going to redirect them to this screen. That's what we're going to use that for. Let me just show you something. So this is step two, but I believe there is another step right here, which is the next public clerk sign in URL, right? So we also have to add that. So clerk knows where to redirect in case something like that happens. So let's add that exact route here. next public clerk sign in URL and we should also have the sign up URL. Let me just confirm inside of environment variables that both of them exist. So clerk sign up URL and clerk sign in URL. The reason we need next public here is because if you want to make an environment variable available to the client, you need to prefix it with next public inside of next JS. That's why they don't have it here. All right. So we've ensured that. And I think that's it for the documentation. We don't have to focus on that anymore. Great. And now I think there's also one more thing, actually, that I want to add in regards to the environment variables here. And it's those. Clerk sign-in fallback redirect URL. The fallback URL to redirect after the user sign-ins if there's no redirect URL to the path already. Right. So this is useful. if the user doesn't have any predefined redirect URL, we're going to create a global fallback. So it always goes to our root page. Even though it says it defaults to slash, I found that when using the clerk model, which is something that I'm going to be doing, that doesn't happen. So we're going to go ahead and add it either way. So let's do next public clerk sign-in fallback URL. Go to the root page and sign up, pullback URL. Go to the root page as well. Yeah. Okay. Even though it says it defaults to here, that wasn't exactly my experience, but with one specific model, which I'm going to show you in a moment once I add that. Okay. So we now have this ready. Great. So now we should be able to log in, but before we do that, how about we go to our localhost 3000 here like this and actually make this sign in or out the button react to whether we are logged in or not. So we can do that by going inside of modules out UI components out button. Let's mark this as use client component. This is very important when needed to be used client. And now let's import user button from clerknext.js, sign in button, signed in, and signed out. So all of these things from clerknext.js. And now we're going to remove the to-do here because we're actually going to do it. So I'm going to wrap the entire thing in a fragment like this. and then what I'm going to do is first I'm going to add if we are signed out if we are signed out that's when we're going to render this right and then I'm going to wrap our button in a sign in button like this there we go so now let me just see I have some errors so that's unused files that's fine if I click on sign in now it will redirect me to this sign in page, right? So that's cool. If you like it that way, that's perfectly fine. But here's a cool thing. You can direct the sign-in button to use the mode model. I like this better. When you click on it, there we go. It shows up this sleek model, right? What I've experienced is that when you use the model, this doesn't really work. So sometimes it will take you like to your account dashboard in Clerk. But if you explicitly set these routes, then it will always lead you back to the root page here. All right. So we now have that. And now let's create a state when we are signed in. So signed in, we'll use the user button component like this. And the user button, well, doesn't really need anything, I think, right? So let's go ahead and try it out. I'm going to click sign in, continue with Google. I'm going to use my account here. I'm going to click continue. and now after this loads I should be redirected and there we go we have the user button and we can update our profile we can remove or change our profile picture we can connect or disconnect our accounts all of those things and inside of your dashboard you should see your first user right here and inside of here you can also you know impersonate that user ban the user delete the user or simply look at their profile and change any information you have about them. So quite useful things you can find here. So now what I want to do is the following. I want to go ahead and add a little to-do here. Add menu items for studio and user profile. So right now we don't have either of those, but Clerks makes it very easy to add multiple items to this user dropdown, even though they are going to internally redirect somewhere. So they're not going to use any, anything Clark related. So that's really cool. Great. So we're going to have that. And now let's go ahead and let's log out. So that works as well. Perfect. And one thing that I actually want to do is I want to go inside of my layout right here, and I want to give the clerk provider an explicit after sign out URL to always go to the root page. I like to be explicit, even though I think this is the default. I like to be explicit so I know exactly that's what's happening. Great. So now what I want to do is I want to go inside of my main section, for example, and let's go ahead and actually do this. To do something on click, let's actually make it open the clerk model. So what we can do is we can import useClerk from clerkNextJS like this. So constClerk will be useClerk. And we can also add userID from useOut. There are many hooks that you can use from clerk. For example, you have useUser and things like that, But I think useAlt is very simply just loads the clerk token, right? It doesn't have to load anything. It will wait for clerk to load, but the hook itself is not really loading anything, right? So let me just escape this. So instead of extracting user ID, you can also use is signed in, for example, like that. And then what you can do here is on click, if we are not signed in, And if this item has the out feature marked, we're going to prevent default. Let's get the event. So prevent default on here. And let's return clerk open sign in. And then here we're going to, well, nothing actually, right? It's just going to override the link feature. So by default is signed in, right? So by default, when someone clicks on the sidebar menu button, it will lead them to the URL. But in case something, this is our main section. So for example, subscriptions should be unclickable for a person that's not logged in. So home button works. If I click on trending, I get a 404. So that technically works. But if I click on subscriptions, there we go. I get a model, right? Let me just go ahead and try and log in. And if I click on subscriptions now, I get a 404. exactly what I wanted. Perfect. So now let's go ahead and let's do the same thing inside of the personal section. So personal section right here, and we can do the same thing. So let me just add the imports here from clerk next JS like this. Let me go ahead and copy this too, add them to the personal section like that. And then I can just copy the on click. And we can remove the to-do comment here. And I think that the same thing should now work. So inside of here, there we go. Perfect. So that works now. What I want to do now is just learn how to protect the routes using the middleware. So right now, our middleware is allowing us to use any route, right? It's not explicitly protecting anything. So how about we create a new route? Let's go inside of home here. Let's call this protected. And I'm going to create a new file inside called page.tsx page. The only authorized users should see this. Or only logged in users, right? So if I now go to localhost 3000 slash protected, even though I am logged out, I can see this page. How can we fix that? Well, very simply, if we go inside of source middleware.ts, we can learn how to use it. So we're going to add create route matcher from here and define const is protected route to use the create route matcher and open an array inside. And inside, we can simply pass slash protected like this. And then we can modify the clerk middleware to be a bit more explicit. So let's get the auth and the request here. And if is protected route, passing the request. In that case, await auth.protect. As simple as that. So now if I refresh this, I am redirected to my sign-in page. So that's why we need the sign-in pages after all. And you can see inside of your URL, you should have the redirect URL to read you back to the protected So if I continue with Google now I am redirected back to the last page that I tried to access right So that how that works And in case you wondering well what if I have a whole sub route that I want to protect right Inside of here, I should have something, right? And if I go inside of page.tsx, let me just copy this. See this sub route, for example. and if I go and log out and if I go to slash protected slash sub route, is that what we called it? We called it something. So something. I can still see this, right? Because my middleware explicitly only protects this route. It doesn't protect slash protected slash something. So what you can do in that case is give it a little parenthesis and then an asterisk. And now if you refresh, all routes under that route will be protected. Great. So I hope you've learned how to use clerk now. It's super, super simple. But there is one, let's call it a problem here. The problem is that at the moment, we are not synchronizing these users to our database. That's what we have to do next. because we're going to need to keep track of which user uploaded which video because these videos will be displayed here, right? And then if I go ahead and change my profile picture, we need a way for that profile picture to update to every video card that my user uploaded. We can only do that by synchronizing our clerk users to our database. And we can do that with web hooks. So this is what we're going to use next. Webhooks right here. We can establish webhooks. And then every time a new user is created, updated, or deleted, it will contact our app and it will tell the database what to do, right? Update that user, delete that user, or create that user in the database. But before we can do that, we're going to have to, well, establish our schema and our database in the first place. So I believe that's what's going to happen in our next chapter. Great, great job. Let's just mark this as completed. There we go. Let's do that. We integrated clerk. We added sign-in screens. We added the user button. We added the middleware. We used the out state on the sidebar sections, and we learned how to protect middlewares. Great, great job. In this chapter, we're going to set up our database. We're going to be using PostgreSQL, which we are going to obtain through Neon database. After we establish our database and have a proper connection string, we're going to integrate Drizzle ORM. After that, we're going to create our first database schema, which will be the schema for our users. We are then going to learn how to migrate those changes to our database on Neon. And we're also going to learn how to use Drizzle Kit, which is very useful because it has a Drizzle Studio, which will allow you to locally browse your database. So before we move on, why Drizzle ORM? So in previous tutorials of mine, I've used Prisma because I really, really like its querying, right? So this picture right here is not Prisma. This is also Drizzle, but I wanted to show you this because this is a very nice type of querying. It reminds me of Mongoose, which is very simple, right? Database query users, find many, and include posts, right? Very simple. But there are a couple of problems with that. First of all, why Drizzle ORM? It is the only ORM with both relational, which is this, and SQL-like query APIs. This is its SQL-like querying. It is also serverless by default, which is something Prisma is not. And this is what I really like about Drizzle, especially when it comes to making tutorials. It is forcing us to understand our queries. What do I mean by that? So if you want to, you can use Drizzle like this. They have both relational API, which is this one. So this is an API similar to Prisma's, and it's great. But to be honest, when you write this, you have no idea what you're doing, right? You have no idea. Is this a subquery? Is this a common table expression? Or is this a join? If it's a join, is it an inner join, a left join, a right join? You have no idea. But if you opt to use SQL-like querying, you're going to have to force yourself to understand what you're doing. So just by using this left join here, I feel like we've gained more understanding of our database and our query than we would with a thousand queries of Prisma. So that's why I'm going to be using drizzle.org and I'm going to be using exclusively SQL-like query APIs, which means all of our queries are going to be like this, almost as if we were writing SQL. So let's start by creating and obtaining our connection URL from neon.tech. So go ahead and visit this website here and go ahead and create an account. Once you've created an account, go ahead and create your project. So I'm going to call this new tube and I'm going to call the database the exact same name. And I'm just going to click create project. I'm not going to modify any auto scaling here. I will just click confirm. And then what I'm going to do here is I'm just going to copy this right here, right? I see they have some options for Next.js, but we are not going to be using this, right? So you can just copy the normal Postgres connection string. So you can click copy snippet like this. And now let's go ahead inside of our .environment.local here, and I will add a database URL, and I will just paste that here. This is probably the most crucial environment variable key you will have. So please do not share it with anyone. As always, this is my test account. I don't care about these keys. I'm going to remove this account afterwards. So that's why I'm showing them to you, but please do not share this with anyone. Keep it for yourself. Great. And we're pretty much done with the Neon dashboard, but I do want to keep it here just so we can see once we actually push our users, we can also explore the tables here. So I just want to teach you how to do that. But for now, let's go ahead and let's focus on adding Drizzle ORM. So you can Google Drizzle ORM to get to its documentation page here. And instead of the get started here, make sure you've selected the new database guide and go ahead and select the neon right here. Great. Here you can also see the basic file structure. So this is our project route. and we're going to have a database folder in which we're going to have our schema. And we're also going to need to have a drizzle config file as well as the index of the actual drizzle. You're going to see all of that in a second, but let's start by installing the necessary packages here. So I'm going to select the bun here just so we have the proper bun commands. We're going to have to add drizzle.oram, neon database serverless, and .environment. And then in our dev dependencies, we're going to add drizzle, kit, and TSX. Technically, since we're using Bun, I'm pretty sure we don't need a TSX, but I'm going to follow the guide just in case we mess it up, right? So I have prepared all of these things inside of NPM so we can see the versions, right? One thing I want to bring to your attention is that you might see this. In the top here, you can see that the version 1.0 is currently 75% done. What does that mean? So they have a public roadmap in which they are going to publish DrizzleKit version 1, which is a very major version. I have contacted the authors behind Drizzle to see how can I ensure longevity for this tutorial. How can I ensure that the moment I upload this video, there won't be any new breaking changes. There is one thing that's happening at the moment, which is the relational queries version 2 API. And I asked the author whether I should wait for this to be released or should I use the current version. And it turns out that this is only going to be out in a couple of weeks after I publish this video. So I got a green light to tell you to install this version. 0.39.0, right? So in this case, I can't really teach you the new relational queries version 2 API, but I'm pretty sure this version will be supported for long enough. So yes, it is a bit important that you install this version if you want to follow the tutorial exactly as I do, or if you want to, you know, explore the latest version yourself. If in the future you see this being a 0.1.0 or something, a major change. You can explore it yourself if you want to. But if you're watching this a bit closer, I prefer you to install the same version as I do. Let's go ahead and install this one by one. I'm going to go ahead and shut down my app. So let's do bun add drizzle ORM at 0.39.0. As you can see, that was published 12 hours ago. And let's go ahead and see what else do we need to install. So we need Neon Database Serverless. So let's add that. And let me see the version for that is 0.10.4. There we go. And then we need the dot environment. So let's add that. The latest version for that is 16.4.7. Great. So let's just ensure we have all the necessary ones. So those three. Great. Let's add them. Perfect. So we have those three. And now let's do bun add dash development. And we're going to add drizzle kit and TSX. So the drizzle kit version is 0.30.3, also published 13 hours ago. And we also need a TSX, which I'm not exactly sure if we need or not. But let's go ahead and install it. So TSX at 4.19.2. Great. Let me go ahead and install this inside of my dev dependencies here. And I think that should be enough. So let me go ahead and do bun run dev now. and now let go ahead and follow the guide button So we have already set up our database URL right here So we don need to do this step Now what we have to do is we have to use that environment variable to create our database util, right? So let's go ahead and do that. I'm going to go inside of source, and I'm going to create a new folder called database. And inside an index.ts, let's import drizzle. from drizzle.org neon.http. And let's export const database drizzle process environment database URL and put an exclamation point at the end to ensure, well, to kind of override the fact that TypeScript doesn't know what this is. We know that it's right here. Great. And of course, you can see that if you need a synchronous connection, you can use our additional connection API where you can specify your driver connection and pass it to the drizzle instance, right? So that's if you wanna use serverless specifically, you can do that as well. In our case, Neon HTTP will do just fine. Great, now what we have to do is we have to create a table and a schema that we can push. In our case, that's gonna be users. Let's go ahead and do that. I'm gonna go inside of database and I'm gonna create schema.ts like this. I will export const users to be pgTable from drizzle.oram slash pgCore, meaning Postgres, right? And let's define the users. So I will always define my table names in multiple entity, right? So not user, it's going to be users like this. Let's go ahead and let's add ID. I'm going to use the uuid from pgCore. I know there's a lot of opinion around UUIDs to its speed, to its long length, right? But I kind of find them to always be a good standard. Of course, if you know better, you can use NanoID, you can use CUID. I believe there are some other newer versions of UUID. The thing is, I've always built my projects with UUIDs without any problems. And I like to only teach you things that I'm certain that work, right? We can experiment with nanoID some other time. If you want to, you can do that. But I'm certain that relations and everything that I have imagined for this project work perfectly with UUIDs. So that's my only choice for that. No other reason. So it's pure preference for me. If you have any other preference, feel free to use that type of ID. Great. So let's go ahead and actually build our user ID here. So we are going to have our primary key, right? But we are almost, well, we are certainly never going to create this via any registration form. This user model will only be created through a webhook, which clerk will fire. Because of that, we are always going to have a clerk ID. So this clerk ID will be what we have here for the user. So, for example, I have one user here and inside of here somewhere, there we go. I have this user ID. So, that's what's going to be here, right? That's what we need. So, we can connect them to. So, I'm going to use the text option from drizzle or mpg core. Make sure you don't accidentally import it from MySQL or something else. I'm going to call this clerk underscore ID. It's always going to be unique and it's always going to be required. Then we're going to have a name, which is also going to be required. And then we're going to have banner URL. Actually, how about this? You don't know what the banner is at the moment, so I'm not going to add it yet. I think it will be a good exercise for us to learn how to modify the schema later on. How about we just add a to-do add banner fields. Let's continue. Let's add image URL. This is something that each user will get when they create an account with clerk. So that is going to be text image URL, not null as well, meaning required. And then we're going to have our handy created at and updated at timestamps. So import the timestamp from PG core, created at, default it to now, and set it as required. And we're going to do the same thing as updated at, just modify this to be updated at like this. There we go. So one thing I want to do now is I want to add an index for the clerk ID because we are often going to search for the user in our database by the current clerk ID that we have. Because we will be able to use clerk's handy server utils. and then we're going to have to query the database to check, okay, this logged in user, we are clerk, what user in the database does this entity belong to? So we're going to do a lot of querying by clerk ID, but clerk ID at the moment is not our primary key. So technically, yeah, maybe we could have put the clerk ID to be our primary key, but I don't know. I'm not really sure about that. So this is what I'm going to do. I'm going to go ahead and create an index on that using unique index from drizzleRMPG core. And I'm going to call it the clerkidx, short for index, dot on t dot clerkid. Like this. There we go. So now I have created an index on this clerkid so I can query faster. Like that. Great. And I believe that we are ready to push this now. But before we can do that, let's continue learning about setting up drizzle. So we have created a table. That's done, right? As you can see, you can also use like varchars if you want to specify the exact length of something. I use text, which is, I'm not sure what is the correct definition. You'll have to forgive me for that, but I use it whenever I'm not sure what I should limit the field to. For example, I have no idea what, how long this image URL is going to be. So I don't have the confidence to set a length to it. I don't want my insertions to start failing all of a sudden. If you are more experienced in this part of the project, feel free to change this to Varchar and put the exact length that you are more familiar with. Great. Now we have to set up our drizzle config file. So create a drizzle.config.ts file in the root of your project and add the following content. Let's go ahead and do that. So in the root of our project, so outside of the source folder, let's add drizzle.config.ts like this. And you can go ahead and copy this entire thing and add it here. So make sure you have installed .environment as I have in the package JSON right here. And one thing we're going to do is we're going to specify which environment file we are using. Because by default, I don't think it knows that we are using .environment.local. It's probably searching for .environment. So what we can do is .environment.config. My apologies, we have to do this. We have to import .environment from .environment like this. And then .environment.config path .environment.local. So now it will be able to read this and find the database URL here. What's important here is that the out is .slash drizzle, that the schema is correct path. So .slash source database schema. Let's see. Slash source database schema right here. Perfect. The dialect is PostgreSQL. That's correct. And this is my database URL. Perfect. That is it. That's all we need for our drizzleconfig.ds. And now what we have to do is we have to run drizzleKit push. Now about this, this is one of many ways you can migrate your changes or apply your changes to the database. This is the most convenient method for fast iteration for solo developers or for tutorials. If you're more familiar with SQL, you might know that there are things called migration files, right? Migration schemas. That is also possible with Drizzle. You can go ahead and read some tips here. For example, alternatively, you can generate migrations using drizzle kit generate command, and then you can apply them using drizzleKit migrate command. And you can learn more about that by clicking here. So now you're going to see how all of that works. In this tutorial, we're going to use the most convenient method, drizzleKit push. Basically, anytime we make a change to our schema, we're just going to do drizzleKit push. Let's go ahead and do that now. I'm going to go ahead and do Bunnix drizzle-kit push like this. And let's see if we get any errors. Looks like everything went find. Pulling schema from the database and changes applied. It means that it correctly found our schema and it correctly pushed to this database URL, which we specified right here. So if I now go ahead inside of my Neon console, you can see that I have zero tables in my public schema. But if I refresh, I now have my users and you can see all the fields which we created previously. The ID, the clerk ID, name, image URL, and the timestamps right here. Perfect. So that works just fine. One cool thing about Drizzle, if you don't know, is that they have very powerful branching feature, right? If you go ahead with this project and create proper staging environments, development environments, preview environments, and production environments, this will come in very handy to you. Especially because you will be able to replicate data from the production database. Now, I know that's a bit controversial, but in fast iteration, at least in my case, that has proven to be insanely useful to have real data that you can test on without actually modifying the production database. So I just want to bring that to your attention. I think that's super cool. They also have the restore feature. They have very cool monitoring so you can see if something is going on with your queries, if you need to have some focus on a slow running query. They have integrations. They have these authorizations, a lot of things that you can explore here. Great. So let's go ahead and see what else do we have here. They are teaching us how to seed and query the database here. We are going to learn how to do that in a moment, but here's what I want to do. What I want to do is learn how to use the DrizzleKit Studio. So let's go ahead and do this. Bunnix, DrizzleKit Studio. Right here is going to say that Drizzle\nStudio is up and running on HTTPS local.drizzle.studio. So if you go here, you should be able to see your studio right here and your users. And if you go ahead, you can go ahead and add a record. So this is not something that we're going to do now. But if you want to bootstrap, if you want to quickly add something, you can do that from here. Great. So that's what I wanted us to learn. Let's go ahead and see. Did we cover everything we need here? We created our Postgres database. We've set up Drizzle ORM. We have created the user schema. We've learned how to migrate the changes, and we've learned how to use DrizzleKit. And we explained why we're using Drizzle ORM. And later on, you're going to see why I really want to use this instead of this right here. Great. So I think that wraps up this chapter. What we are going to do next is we are going to configure a local tunnel, which will be able to communicate with our clerk webhook right here, our application, and our database. So every time a new user is created in clerk, what we are going to do is we're going to go ahead and actually populate this table right here. And then inside of our drizzle studio, we're going to be able to finally see some users here. Great, great job. In this chapter, our goal is to synchronize our clerk accounts with our user's database. In order to do that, we're going to have to implement our very first webhook. In order to do that, we're going to need to establish a local tunnel. There are a lot of solutions online for local tunnels, but I would highly recommend using Ngrok. It is by far the most reliable method of doing this. And one very, very cool thing is that they will give you a static domain, right? So this is very useful because usually every time you start a local tunnel using some different tools, they give you a different URL, which means that you have to change your whole configuration in all the websites where the webhook is coming from. But if you use a static domain, you can just remember it once, put it once, and it's always going to work. Also, just to clarify, we're going to need Ngrok only for development. In production, that's not going to be a problem because we will have our URL, our website.com. But in development mode, we don't have a URL. We cannot tell a production app to connect to our local host app, right? That's why we need to expose our local host app to the public. And for that, we need something like NGROC. So again, NGROC is not required. Static domain is not required, but both of these make development much, much easier. Let's go ahead and let's do that first. So I'm going to go ahead and create an account on NGROC right here. Once you are logged in, you're going to see all the instructions you need. For example, on macOS, which it recognized, I can install it using brew and then I can add my token. Keep in mind, again, you should not share this token with anyone. This is my dummy account, which I just created and I did not even connect this to my PC, but you should not share this with anyone, right? Or you can just use the download button. And the same thing is true if you're using Windows or Linux or any other solution. Once you have ngrok installed, you should go ahead and confirm that. So let's go ahead and check my version. So ngrok 3.19.0 is my version. Basically, ngrok should not throw you any errors. It should work as a command. The next thing you have to do is you have to establish a static domain. So instead of your ngrok dashboard here, let me zoom in a bit, you should find domains and go ahead and click create domain. And inside of here, you're going to get one, only one, free static domain. So for each account you create on Ngrok, you're going to have this static domain like this. So now if you go ahead, for example, and do the following. Yeah, this is not going to work for me. So for you, this is going to work. So you can just copy whatever domain you have here. But I have to use a different one because as I said, this is my dummy account. I didn't connect this. So let's do this. In one terminal, run your application on localhost 3000. And in another, go ahead and run that ngrok command. Let me just go ahead and why can't I do this? Okay, so ngrok. And it seems like I have to do this. So let me just ngrok like this. And I have to modify my URL. And in case this doesn't work for you, don't worry, because I will show you one little trick. So if I go ahead and change this to my proper domain and change this to look at the port 3000, because we are running on 3000 here in another terminal, it will start my tunnel. But when I was developing this, I had a bug that if I typed dash dash URL, I kept getting an error that URL doesn't exist. In that case, you can try dash dash domain and it should still work, right? So it looks like they have changed that name, but they still have backwards compatibility. So it will depend on the version of Ngrok you have. So if dash dash URL throws an error, you can use dash dash domain and you will get the same thing. So go ahead and run this. And now, if you go ahead and expand this, you should be able to see the URL where your application is exposed to the public. So right now, if I go here, you will see that it will load the app which we have been building previously. There we go. So YouTube with our basic layout here. Perfect. So just confirm that this is working. Confirm that you can visit this new URL here. Let me go ahead and just enable developer mode here. There we go. So this is my URL. Evolve the humbly gopher. Whatever that means, it's random, right? Great. So one thing that's kind of a hassle in development is that you have to remember to start this every time you need local development. But I found a neat little trick you can do. And in order to do that, we first have to do, we have to install concurrently. So let me just show you the NPM version of this. So concurrently enables you to run multiple commands concurrently. The issue is you probably think, okay, well, I know how they're in two commands, but that's not running them concurrently. And attempting to run a webhook and NPM run dev at the same time will fail. So that's why you need concurrently. So let's go ahead and add concurrently with version 9.1.2. So I'm going to shut down both of my apps here, and I'm going to do bun add concurrently at 9.1.2. There we go. And then what I'm going to do is I'm going to go inside of my package.json. You can confirm that you have concurrently installed right here. And then go ahead and do the following. Add that webhook like this. and dev webhook is going to be your ngrok command. Let me try ngrok. There we go. So basically this command which you just previously run, right? Make sure that the port is 3000 and make sure that you're using the proper dash dash URL or dash dash domain. And simply add that here to be dev webhook. So now if you actually try and do bun run dev webhook, it will start the ngrok. Great, so that's great. That's now shorter. You didn't have to remember the whole domain thing. But let me show you another little trick you can do. You can add dev all like this. And then you can run concurrently. We can run it like this because it is installed inside of this project, right? Otherwise, you would have to have it globally installed. So concurrently. And then you have to add a backwards slash because you have to escape this regex. And inside of here, go ahead and run bun run dev webhook. So that's the first one we're going to run. And we have to escape this like this. This is a bit confusing, but yeah. Backwards slash quotes and backwards slash and then add a quote. Basically, we have to escape this quotes, right? And then you can just go ahead and do the same thing again. So backwards slash and then bun run dev and then backwards slash and close this. So now it's going to run together the webhook and our next dev. So if you go ahead and try it now, let me exit this and do bun run dev, comma, colon, all. It will concurrently run the webhook and bun run dev. So if I go ahead and try going on my Evolved Humbly Gopher webhook, you can see that it should be fully available. It's refreshing now. There we go. So no errors. This route definitely exists and I can log in here. Great. And if I go inside of my localhost here, localhost works as well. So both of my apps are now working with one command. So that's why I wanted you to have a static URL with ngrok because it's super simple. Again, you can change this to domain if URL is not working for you. And if you forgot where I got this from, it's right here, right? So inside of your ngrok domains and you have one single domain here, you can click start a tunnel and then you will get this command right here. Just make sure to change the port from 80 to 3000 like this. So this should work because ngrok is globally available, right? So if I type ngrok, you can see it's globally available. Concurrently, it's not globally available for me, as you can see, but it works here because we have it installed in our project. And yeah, technically, we can move this to dev dependencies. I should have probably installed it in dev dependencies. But yeah, it's okay. It's not going to be a problem if it's in my dependencies. It's not too big of a package. Great. So we now have that. Now let go ahead and let do the following We have to go inside of Clerk Dashboard We have to go inside of Configure right here And we have to go inside of our webhooks So let me just find the webhooks. There we go right here. And now we have to add an endpoint. So this will be our URL right here. So let's just add HTTPS. your static URL, which you got from ngrok, slash, this is going to be API, slash users. I think that that's where we're going to put it. Please excuse me for a second. I'm just confirming that with the original source code. All right, let's also add slash webhook here. So it is a bit more precise. So slash API slash users slash webhook. Make sure it is like this. So this will be the equivalent to localhost 3000 slash API webhook, right? In case you're confused. But just make sure that inside of here, you have to put your actual local tunnel because this app has no idea what localhost is, right? That's only on yours. Great. And now inside of here, if you want to, you can add descriptions. For example, user synchronization. You can write that if you want. And then inside of here, you can go ahead and find what you need. In our case, I need everything user related. So created, created at edge, deleted and updated. I don't really need anything else here. Great. And let's just click create. And what's going to happen now is that you can always, of course, change this URL if you accidentally wrote the wrong one. And now let's go ahead and copy this sign-in secret, right? So you have to click on the little eye icon. You will see the secret. Again, do not share this secret with anyone. Go inside of your .environment file. And I'm just going to add this with my clerk secrets. So signing secret will be my new signing secret like this. There we go. So how about we prefix it with clerk, sign in secret. And yeah, no need for next public in front of this because this should not be exposed to the client, right? This should be on the backend only. Great. And now what we have to do is the following. We have to take a look at the documentation for the webhooks. You can find them right here in the webhooks section here. And you can see and learn more about how they work. why do they use Spix, some payload structure. And if you scroll a bit down, you're going to find synchronized data to your database. And you can find a little guide here. This guide will give us a little template for the API route, which we need to create. And you can see they also use Ngrok precisely because it has this very useful domain, right? This very useful static domain. So we already did all of these steps. That's great. We didn't have to do that. We even created this cool little dev all, which starts both the webhook and the project at the same time. But now we have to set up the webhook, which we actually did. And we subscribed to all events for the user, not just user created. We added the sign-in secret to the environment local. So we did this. That is fine. And now it says to set the webhook route as the public in your middleware. We don't have to do that, right? Because remember, our middleware, by default, allows all routes, right? So I can only protect that route. I cannot make it more public than it already is. Now let's go ahead and let's add SWIX. So let me go ahead and click here. I think this will open NPM. It looks like 1.45.1 is the current version. So let's do 1. add SWIX at 1. I already forgot 45.1. there we go once we have swix let's take a look at what else we need so we need to create the actual endpoint all right so they have their own endpoint here we're going to do a different one so let's go inside of source instead of app folder and i didn't really demonstrate how to create routes in next js that's something i forgot but it is as simple as creating uh client routes right Instead of having page file name, you have a route.ts file name. So for example, if you wanted to create a test like this inside route.ts, you could very simply write export const get and return response. Can I just write hello? I think this should work. So if you now go to localhost 3000 slash test, do I have my app running? I do not. Bun run dev all. So from now on, you're going to use bun run dev all, not bun run dev, right? And there we go. You can see how it works, right? So that's how we create an API route inside of the Next.js app router. Very simple, right? So remove this test for now. And now where I'm going to put my API routes is not required, but I quite like them to be instead of an API folder. So yeah, it's not required, but I think it makes sense to keep them here. And now let's go ahead and let's create our users and then let's create our webhook right here, right? So this is basically slash API slash users slash webhook. So it needs to match whatever you wrote here, slash API, slash users, slash webhook. And inside of here, add a route.ds file. Great. And now we can copy this example they have right here. In case you decided not to follow the documentation, right, or just want to see me typing, don't worry, I'm going to pause the screen so you will be able to see each part if you want to do it like that. So we first import webhook from swix, headers from next headers, and webhook event from clerk next.js server. Inside, we export an asynchronous post function. So similarly to how I just demonstrated the get function, that's how you do post function here. One thing which we immediately need to modify is the process environment signing secret. That is because inside of our dot environment right here, I've named it clerk signing secret. just in case we have multiple signing secrets, I think it makes sense to add a prefix here. So I'm going to change this process.environment to use clerk signing secret. And I'm also going to modify this warning here to add clerk signing secret to .environment or .environment local. The rest of the naming, which is signing secret, does not matter because it's a constant. All it matters is that it reads the clerk signing secret here. Great. Inside of here, we initialize the new webhook using that signing secret. Again, the webhook comes from Svix, which they use to retry webhooks and have a very reliable way of using them. We obtain all the necessary headers, which we are going to use to validate this request, because this request will be coming not from a user, but from a random server. So we need to validate that this request is actually clerk telling us, hey, new user was created. What do you want to do with that user? We don't want anyone else to spam us. So if any of these things are missing from the headers, it most likely means that someone is just trying to access this endpoint and it's not a webhook handler. If we pass this validation, we are going to get the body. So we are going to get the JSON version of the body, but we are also going to get the text version of the body. And then we are going to verify our webhook with all this information above the Svix ID, the timestamp, and the signature, all combined with our unique signing secret. So this is a very, very secure way of only allowing our specific instance of clerk webhook from contacting our application. No one else can force themselves and trigger this webhook and thus manipulate our data in an unwanted way. So that's why we are doing this kind of validation. If this fails, we throw an error and we say, I cannot verify that this is who you think it is. I'm just going to break the function. Otherwise, we can finally access our events here. So you can see that we have received the webhook with ID and the event type of event type like this. So I believe that a clerk might allow you to test this out. I'm not sure. Is there a way to test it out? Let's see. Maybe event catalog logs. If I go inside of user, user created. Okay, I thought that maybe they're going to give me some. Yeah, I have no idea. Oh, okay. User created. Okay, we can see the body, but not exactly what I want. All right. So this is what we are going to do now. We can remove, oh, you can leave this console logs if they help you understand. But this is what I'm going to do. I'm going to check if event.type, my apologies, if event type is identical to user.created. You can see we have autocomplete here because of TypeScript. In that case, what I'm going to do is I'm going to await database, which we can import from database, right? This comes from our recently created Drizzle database here. So we're going to call that database and we're going to insert into users. You can import users from database schema, right? This constant, which we have exported right here. Make sure you have exported it as well. Into users, we are going to insert the following values. where first of all, we're going to give it a clerk ID of data.id. My apologies, we don't have data here. Yeah, because it's only the structuring ID here. I don't like that. So I'm just going to call this data like this. And then this can be data.id if that's easier for any way, right? So data transfer, my apologies, data.id will be the clerk ID like that. Name here will be data first underscore name and data last underscore name. And image URL will be data image URL Now we are getting some errors here and I believe that because data can be all of these things but we know that it going to be user JSON if it inside of this type of web hook. So, what I'm going to do is, let me see, Ken, if I move it here, there we go, looks like that solves the issue, because if you read the data inside of this if clause, it knows more precisely what it is. So it's a user JSON. Whereas if you move it outside, it can be all of these things. Great. So just move it inside. And well, you can remove these console logs. So they don't cause any issues. Great. So we now have the data from event.data. And yeah, if you want to, you can destructure that like this. And this is why I told you to choose Google as the only sign-in option because if you chose email, I think you're not going to have these properties. So you're not going to be able to create a nice name for your user, right? So if you use Google Login, I think you will almost certainly have first name and last name. Otherwise, all of your users will be called null null because this can be null. And since we are using this inside of template literals, it's just going to show you null now. So if that happens, it's most likely because of the provider you're using. So what you can do if you really care about it, you can do a name and then you can check if data first name is missing. In that case, you can use data email addresses and then get the first and then get the email address. But as far as I know, So this can also be null maybe or maybe not. Yeah, basically just be careful with the provider you chose. All right. So this is enough for the user created. Now let's do event type user deleted. I'm going to destructure data from the event here. And I'm going to do await database delete users where. and now we have to use drizzle.org and we have to import equals from drizzle.org here. We're very simply going to target whatever the users.clerk ID has data.id. Let's just see if I did this correctly. So data.id can be string or undefined. All right, so this is what I'm going to do. If for any reason whatsoever, we cannot access data.id, we can only throw not an error, but we can return new response. And let me see, how do I throw an error? All right, I just specify the status. And I will just say missing user ID like this. So if we are, for any reason we don't have this, which I don't know how exactly can happen, we don't know which user to delete. Because remember, when we created the user, we stored that data.id, which in here clearly always exists, and we store it inside of clerk ID field. So we have to find by that clerk ID whatever we have in the current data ID. All right. So now one more thing that we need is if event type is user updated like this. In that case, we're going to do await database update users. Go ahead and set the name and you can just copy or you can just copy two of this. Right. So the only thing we are not going to modify. Let me destructure the data from the event. The only thing we're not going to modify is the clerk ID. Instead, we're going to use it to specify which user to update. So clerk ID field and we query by data.id. There we go. So that is our webhook. Make sure to return the new response, successful response at the end. Otherwise, all of your webhooks will fail by default, right? So this is quite important that you have that here. And I believe this is it. Let's go ahead and try it out now. So this is what we have to do. Go ahead and make sure you have bun run dev all running. Make sure that you can go ahead and find this static URL. Make sure that you can visit it. And it should load your app, right? This URL should not fail. Great. Now, the second thing we should do is the following. we should go inside of dashboards, clerk.com, instead of your users and delete whatever user you have. So just completely remove them. What's going to happen now already, I think, is that it will attempt to go to slash API users webhook. You can see that it got to API users webhook 200 right here. And you can see the status of that inside of configure. You can find webhooks right here. What I think is going to happen now is that this webhook is failing, I think. All right, so it looks like it is okay. Let's see why does it say that it's okay. Should have it failed. Let's see. Await delete users. Yeah, perhaps this doesn't throw an error. It maybe just returns back null. All right, I mean, it's okay. We don't really have to throw an error if the user didn't exist in the first place. Now let's try and see, do the other events work? In order to try that out, go ahead and run drizzle kit studio. So let me go to local.drizzle.studio. Right now, I have zero users inside of my database, as you can see. So what I'm going to do is go to localhost 3000. Don't go to your local tunnel. So go directly to localhost 3000 here and go ahead and create a new account. I'm testing with Google, so I know that I will have name and surname for this user. So let's go ahead and try it out. I'm getting created. There we go. And now if I go inside of my Drizzle Studio and refresh, hopefully, there we go. We have a new user record and you can see it says one right here as well. So we have the clerk ID, the name John Doe, my image URL created that, updated that. We have all of those things right here. Great. So now what's interesting is that if you go ahead and take a look inside of here, you should see this successful route. Let's see user created. So this was successful. Great. And now if you, for example, manage your account and update the profile picture. Let's go ahead. I'm going to try and upload something. So here's my new picture, right? And what will happen now is that I should get another post request API users web hook here. And I should also have another log here, user updated. As you can see, it seems to work just fine. So user has now been updated. And inside of my drizzle studio, well, you won't really see any difference. But this image URL has been updated, right? It is showing this image right here, which was not what was previously my image. So great. All we have to check now is does deleting the user also works. So let's go inside of dashboard here instead of users and let's delete this user from here. And now what should happen inside of my Drizzle Studio is that I no longer have that user here. And that is true. Amazing, amazing job. So remember, from now on, every time you start your app, you're not going to use bunrundev, you will do bunrundev.org. This will be crucial, otherwise your data will not be synchronized. And then the best course of action is to just dump your database. Of course, this is only for development. This will not happen in production because in production, this will just be your URL, whatever your app is hosted on. This is a solution for local development. This is the only way to do it, actually. There's no other way of doing this. Great, great, great job. Let's see if we did everything we planned. So we created an NGROC account. We obtained a static domain. We added a script to concurrently run a local tunnel in our app. We created the user's webhook, and we connected the webhook to Clerk Dashboard. That's it. We did chapter five. Great, great job. The goal of this chapter is to integrate tRPC inside of our project. So why did I even choose tRPC for this project? Well, first of all, end-to-end type safety. You've probably noticed that this is something that I'm quite fond of, especially in some of my previous tutorials, where I chose to integrate HONO inside of our Next.js project. And then I leveraged HONO's RPC procedures so that I can have very nice and type-safe API routes, results, mutations, queries, all of those things. Well, TRPC is, I would say, a step ahead of that, precisely comparing to HONO RPC. I'm not comparing TRPC with HONO, right? Those are two different things. I'm comparing HONO RPC with TRPC. In this case, TRPC is a bit superior. I would definitely say so. Specifically because we have a much, much closer integration to React query, which allows us to use use query and use mutation and all of those other hooks you've heard of coming from use query natively. What do I mean natively? I mean, for every single procedure that we create inside of TRPC, we will automatically have use query, use mutation, and all the relevant queries and functions and hooks generated automatically. In my Hona RPC integration, I had to write these myself. But with TRPC, they will come generated out of the box. But none of that is the actual reason why I chose TRPC. The actual reason I chose TRPC is because version 11 of TRPC finally allows us to do authenticated prefetching. So no matter how much I liked Hono RPC one thing that Hono RPC was not able to do was do an API call inside of a server component which is basically this right here right Imagine this said Hono instead of TRPC. That was not possible. The reason it was not possible is because what Hono did in the background is it would initialize a fetch API call in a server component. That's completely fine, Nothing wrong with that, but that's not a native RPC call on the server. It is a server which is calling fetch to my very own server, right? Not exactly elegant nor optimized, but the most important thing that happened is that the headers got lost, meaning that in Hono, it was not possible to prefetch authenticated queries. We could only prefetch public database queries. We could never do a prefetching for something that specific to one user. So that was a big issue. And when I found out that version 11 of TRPC has solved this issue, I had to try it. And trust me, it's worth it. I think you're absolutely going to love it. It is understandable if you are a bit confused by this intro. This is all very new, especially if you've never heard about prefetching or server components in the first place. So I'm going to try my best to explain it. And we're going to slowly build around this concept and get familiar with it. So I hope it will finally click for you as well as it did for me while I was building this. So why do I care about prefetching that much? First of all, I very, very much like the render as you fetch concept, because this specifically means faster load times. And they are definitely visible, right? When I tested a prefetched server components versus just a client component fetching for the first time, the prefetched server component was visibly much, much faster. Besides that, we also have parallel data loading. And one concept that I really like, which is leveraging React server components as loaders. So this is where I finally saw React server components as, okay, this is what I want to use them for every single time. This is why they are so powerful for. And this is the pattern that we're going to try to achieve. Keep in mind that I made this graph, And I have no idea if this graph is breaking all the rules of any other, sorry, not graph, the diagram, right? I'm just trying to translate what's in my head inside of here. So I hope you will understand. Basically, we're going to start with the server component, which will use TRPC version 11 to prefetch data. And it can also prefetch authenticated data, which is something which was not possible before. So I never really entertained this idea of server-first data pattern. But now that we can do authenticated calls, this is completely doable, right? So we're going to use a server component to prefetch the data. Thanks to TRPC's very close React query integration, it will automatically be set inside of React query's data cache. And then we're going to create a hydrate client component in which we are going to wrap our client component so that our client component can easily access the cache, which was prefetched right here. And then in case we need to revalidate our client component, we are going to keep that inside of this layer right here between the data cache and the client component. So we are not going to go back to server components. We're not going to do any revalidate or revalidate path or any of those things, right? It's going to be like this. We're going to leverage this new thing, server components to speed up our app. And we are going to continue using our familiar client components, just as we've learned to use them in years of previous React tutorials, right? This is a pattern that I really like because we are leveraging server components to truly speed up our application, but we are also leveraging our existing knowledge of client components to continue doing everything we know and love about them. If this diagram doesn't make sense, I prepared a code snippet here. So inside of here, we have an example videos, dynamic video ID page.dsx route. And inside of here, I have a server component. And inside of here, this is what we're going to do. We're going to call trpc.videos.prefetch. And we're going to prefetch a single video ID. And then we're going to use this hydrate client, which we're going to create inside of this trpc setup to wrap our video client component, right? You can see that in this video client component, the only thing I'm passing is the video ID prop. I'm not waiting for any data to be loaded here. I'm just prefetching the data in the server component. And then once it's been preloaded from server and cache, what I will be able to do in the video client here is use the RPC.videos that use Suspense Query and simply pass that video ID. And this right here is already populated. So you can see that inside of my videos, I can immediately do dot map. I don't have to do, if this was, you know, waiting for load, they would have to do videos question mark dot map, right? Because it's possible for videos to be undefined. Well, not possible in this case, because the moment this video client component is rendered, this has already been prefetched. So that's why this is a much faster concept for us to work with. And of course, we're also going to leverage suspense and error boundaries to display nice loading states. And then we're going to try and do both of this. We're going to try one trpc.videos.normal use query. And we're going to do one with the prefetch here. And we're going to see which one appears faster. And let me already tell you the answer. This one will be much, much faster. That being said, this is a fairly new pattern, right? I have contacted the creators of TRPC, you know, for their opinions. Is this something that I can teach? Is this an invalid pattern? And I've gotten a confirmation from them, you know, that this is an okay pattern. This is, as far as they know, the best way to do this kind of pattern. So it's not something I have invented myself and now, you know, trying to force onto you. Great. So let's go ahead and try and do this. So for this chapter, we're not really going to have any complex queries, but I do hope that we will at least be able to make this kind of example. One server component prefetching and then a client component using that prefetched data inside of it. Great. So we're going to start by going to TRPC website. So move fast and break nothing end-to-end type safe APIs made easy. So depending on when you are watching this, you might or might not see this version right here, which is you are looking at the RPC version 11, right? So if you click here to see the list of changes for version 11, you go ahead and click here. Here's the thing. Version 11 is not currently out, meaning that if you go ahead and go write npm info, for example. let me see one package. So, trpc slash server, for example, if I do this, let me just find, there we go. You can see that the latest version is 10.45.2, right? That is the latest version. But instead of here, it clearly says that version 11 is the newest version. Well, it's not fully out. Then this is the state of it. This is a current work in progress, representing version 11, but the functionality, the current functionality of version 11 is stable and it can be used in production. The reason they are adding this alert is because they might have some small breaking API changes between patches until they reach this version right here. So this is what we are going to do. We're going to go ahead and click on docs here. Let's click on quick start and let's go ahead and find, okay, we actually have using Next.js here. I would highly recommend that you go to the documentation as well so you can see exactly what I'm seeing here. So click on using Next.js right here. We can click on setup here. And then we have this caution here. This guide is for Next.js pages router. If you're using Next.js app router with server components, check out the React server component docs. So you can just click here and that will basically open client usage, React query integration, server components. So this is where we are going to start. Great. So first of all, we're going to need to install all of these packages. trpc server at next, trpc client at next, trpc react query next, and then tan stacked react query zod, which we already have, or maybe actually we don't have zod. And basically we're going to have install all of these things. So I'm going to go ahead and do them one by one because I want to see the specific versions of this. So I'm going to do npm info like this because I want to see the current next version, which is 11.0.0-RC730. So let's go ahead and do that. One add at the rpc slash is it server there we go so at the rpc slash server at 11.0.0 dash rc730 and i would recommend that you follow you know exact versions as i'm using and i'm pretty sure that all others are going to be here as well so let me see the rpc client yeah all of them are 11.0.0 rc730 but i will check all of them just in case so that is the rpc slash client let me just confirm it is the rpc slash client okay there we go then the same thing for the rpc react query so let me go ahead and check with npm info the latest is this one so npm my apologies bun add add trpc slash react-query at 11.0.0-rc730 there we go uh then we use the latest version of tan stack react query uh it's pretty important that they match right so what i'm going to do is i'm going to do the same thing info for and stack react query at latest. Let's see the\nThis one is, I'm not sure what should I be looking at here. How about I just visit npm.js? Let me go ahead and search for it here. 10 stack react query. This one. So 5.65.1. That's the version we are going to be using here. So 1 add 10 stack react query. 5.65.1 like this. And let's see what else do we have to do. We need Zod client only and server only. Now, I don't think these matter too much. So, we can just add all of this because you can see their versions. So, 3.24.1, 0.0.1, and 0.0.1 here as well. If you want to, you can specify this version specifically, but I don't think any of these can introduce any too much of a breaking change, right? Great. So let me go ahead and go inside of my package JSON here just to confirm my changes. So I have 10 stack React query 5.65.1. I have trpc client, which is 11.0.0-RC730. And all the other TRPC packages like React Query and Server are exactly the same. My client only is 0.0.1. And I have also added Server only, which is also 0.0.1. Now let's continue with the docs here. So first of all, it says to start with creating a TRPC router. So let's see if we can do something like that. Yes, you can just click on view sample backend and you can just copy this entire thing. So go ahead inside of your source and inside of here, go ahead and create trpc like this. And inside create init.ds and you can just paste this inside. So it's going to import init trpc from trpc server. It's going to import cache from react. It will create a very basic TRPC context. In here, they just give you an example, like a user ID that you will destructure from your auth endpoint, right? And they create a basic TRPC init project here, and they export some procedures and some factories, which you will be able to use later on. So make sure that you have this very simple init file. Let's go back to the docs and see what we have to add next. So we have trpc init.ts, and now we just have to create like a basic hello route here. So we're going to do that inside of trpc folder. We're going to create routers and then underscore app.ts. So let's just copy this here. Inside of trpc, we're going to create routers. And inside of here, an underscore app.ts and just add it here. and you should have no errors here because we have the base procedure from here we have create the rpc router export from here and we just have a very very basic you know hello procedure base procedure means your normal public procedure the input here is basically a zod validation of what this procedure expects to receive and this is what it will return right so we just have a very basic API endpoint here. So we can close the sample back and I believe that's all we have to do here. My apologies, not all we have to do. We also have to properly set up our app folder, API, trpc, and then the catch route here. So let's go inside of our app folder, API, and let's go ahead and see. So we have to create slash trpc. My apologies. So trpc, and then we need to create dynamic trpc and then inside route.ts like this and we have to copy this and we're going to have to fix this because they are using this tilde as the alias but we are using the at sign as the alias so we need to create trpc context from trpc init which we have right here, create the RPC context, and we need the app router from the RPC routers app right here. So that is our basic setup here. And now that we have a minimalist backend setup here, right, I think we can finally close this. There we go. And now we can go ahead and continue with the setup. And that is to create a query client factory. So let's create a shared file inside of our trpc folder called query-client.ts. Let's go ahead and do that. So inside of our trpc folder, we are going to create query-client.tsx. Actually, it says .ts, so let's call it .ts properly. And let's just copy this and put it inside like this. It looks like we are missing the module super JSON. Yeah, I'm going to comment it out for now. You can comment it out as well because you can see it's commented out here, but we are going to enable this later. So it's okay. It can be commented out for now. So a very simple query client here. This isn't the RPC specific. This is tan stacked react query specific. All right. So we have our query client factory and you can read more about some default options they have set here. I decided not to modify anything here. I was quite satisfied with whatever were the default settings. One thing that we will change is we are going to enable data serialization. That's so we can safely pass things like date between server component and the client component. If you don't serialize your data, you will not be able to do that. So that's why they prepare the super JSON for you. Besides here, we're also going to have to, I believe, enable that inside of TRPC init. There we go. Inside of here, we also need to define the transformer. But we're going to come to that in a moment. Great. And now we're going to have to create a TRPC client for client components. So what does this mean? Well, this brings us back to this example right here. we're going to have two TRPC instances. One will be for prefetching. So this TRPC right here, let me grab my pen. So this TRPC right here will be a server instance of TRPC. And this TRPC right here will be a client. I have no idea why that has darkened, but basically this one here will be a client instance of TRPC. So now we need to create both of those. This one will come from import, you know, TRPC slash client. And this one will come from import trpc server. So let's go ahead and first create a client one. So this is quite a long one. I'm, of course, going to pause my screen in case you want to type it out. But, you know, you can also find it in the documentation or in the source code. So let's create trpc slash client dot dsx. Client dot dsx like this. And I'm going to go ahead and just, you know, show you a bit. So make sure we mark this as use client so we can mount the provider from a server component. These are our imports. Make sure you are getting no errors here specifically for this relative imports. So if this is red, it means that you put your client.tsx in an incorrect place or your router. Something is incorrect. If everything is inside of the TRPC folder, there should be no errors here. After that, we create a TRPC React instance here. And then we simply combine this with our React query instance here. So basically, this is what we are going to call for our client components right here. And this is where we're also going to have to enable our transformer. A few gotchas here is, well, this part. So they use Vercel URL as the environment variable. If you choose to deploy somewhere other than Vercel, you will obviously have to change this. You can change this in a couple of ways. It's not that difficult. You can also use process.environmentNextPublicAppURL. And then inside of your environment.local, you would simply do nextPublicAppURL. By default, inside of here, it's going to be localhost 3000. And then when you deploy, you would change this to be your actual application, which would be www.codewithantonio.com. That's how you would do it if you are not deploying on Vercel. But in case you are deploying on Vercel, you can just leave this to be Vercel URL and let it fall back to localhost 3000 when you're in development. And everything's going to be just fine. So in order to complete this tutorial, I would recommend just deploying on Vercel because it's super simple and it's guaranteed to work. And then if you want to explore it yourself, just add a little to do here. Modify for outside Vercel deployment. Just, you know, a little note for yourself in case you plan on experimenting with this later so you don't get confused. Oh, why is it broken all of a sudden? And obviously, this is also important. this get URL leads to slash API slash drpc, which is obviously targeting our app folder app drpc, which we just created right now. So if you, for whatever reason, decided to name this something else, or perhaps made a typo here, make sure to fix it. And to be specific, you need to change it here as well. And you will also need to change it inside of this client.tsx. Great. So I hope that I've shown you enough of this file so that you can write it yourself. You can also find it in the documentation. You can find it in my source code as well. And now that we have finished this step, I believe what's left is to create a RPC caller for server components. And this is where the magic happens, right? Usually, not single other RPC had this solved, right? We were simply not able to natively call an RPC instead of a server component, which is basically this part, right? We were not able to do this This was always just a normal fetch or API request which loses all the headers and has no idea which user made this call But thanks to this magic of the RPC caller, we can finally do that. So let's create the RPC slash server dot TSX on the same place, on the same level as client. So server dot TSX. And let's copy this. Let's add it inside. Make sure you import server only in the beginning. What this will do is it will throw an error, I think, both in local development and in production. I think it will even prevent the build from happening if it detects this file being imported in any way, even abstractly. So without your explicit knowledge or approval, it will throw an error saying, hey, this file somehow found its way into client side. And that's dangerous. you can't allow that. So this is to ensure that our server-only TRPC instance can only be called on the server, so no malicious requests can be made. Great. So this one is quite simpler. Again, just confirm you have no errors in this relative imports, and this new query client should now be used here as well. Great. So, so far, you should have the following. The TRPC folder, the routers with a very simple HelloBase procedure. You should have a bit larger client.tsx component here, an init file, a query client, and server.tsx. And you should also have app API trpc, trpcroute.ts. And now you can finally use your API. In here, they have some examples. So how about we go ahead and try and prefetch hello here. And now we're going to try out all of these examples. So first of all, what I'm going to do is I'm going to do bun run dev all. Remember, we have this new dev all command. We are no longer running just a dev. So I will just try to load my app, you know, just to see what the state is. So this one says I will load videos in the future. So how about we go inside of the app folder, home page.dsx. and let's do something simple. Let's mark this as use client like this. So this is now if you refresh a client component. And now what you should be able to do is you should be able to extract data from tRPC, which you will import from tRPC client, right? This is this big component. tRPC.hello.useQuery. And it expects a message, I think. Let's see, what does it expect? A text, all right. So let's just pass in text, Antonio, like this. And then I will say this client component says data. And is it data.greeting, my apologies. And you can see that he already put a little exclamation question mark here. So let me go ahead and try. Oh, yes, I think we forgot one important thing. How did I miss that? and let's see, how did I miss that? We created this for client components. And pretty sure, oh yes, mount the provider in the root of your application. Okay, so that's a big miss by me. Let's go ahead and do that. So we're going to go ahead inside of our app folder and let's go inside of layout.tsx and inside of body, let's add tRPC provider and wrap the children inside of that. So tRPC provider comes from tRPC slash client. Let me just move it here. So make sure that you have imported the tRPC provider. It should be this component right here. Great. And make sure it's wrapping your children. So you're doing this inside of the app layout, root layout. Now go back inside of homepage.dsx. And I think that now it says client component says, hello, Antonio, right? If I change this to Antonio2, it says, hello, Antonio2. And now the way I refresh is I use this. I'm not sure if you can see, but I use shift command R, which basically clears my cookies and my cache, right? So I always do that because if you just refresh, sometimes it will have cache and you won't really see the loading happening. But you can see how right now, it's always kind of loading this. This is your normal, usual way of loading the data. Nothing special with this. Now, let's try an alternative method, which I was talking to you about. Let's remove this use client. And let's go ahead and instead of importing TRPC client, let's import the RPC server. And what we can do here is, first of all, we can mark this as an asynchronous function, for example. And we can do data, the RPC, hello. And let's see, do I just call it like this? I think yes. Text, Antonio. And we can just await it. And then right now, it will be the same thing, except I think we don't need this, right? So you can see that now what we're doing is we are literally fetching on the backend. So this is a server component now fetching. You can see how we await here. So yeah, once we await, of course, it's going to exist, right? There's no doubt about this. And I just remembered one important thing. I'm sorry, but I have to stop here because some of you might have some issues. Let me just bring your attention to one thing. I promise we're going to come back to what I was demonstrating. but something did happen to me when I initialized the TRPC and I wasn't sure how to fix it exactly. In case, when you hover over your data here, you should be seeing data greeting string. When you hover over text, you should see text string. When you hover over hello, you should also see all of these proper types. What can happen is that you are constantly seeing any. I'm going to try and find if they have some troubleshooting for that. There we go. It doesn't work. I'm getting any everywhere. If that's happening to you, you should go ahead and read this part right here and try and do these things. If you follow the entire setup the same way I did, it will probably work. But this is what finally fixed it for me. It was adding this too, right? So TypeScript.tsdk to use this TypeScript and TypeScript enable prompt use workspace.tsdk. Let's add both of those to our VS Code settings.json. So I promise I'm going to come back, but this is quite important because some people will stop watching here and try to debug themselves. And perhaps they won't know that I explained the fix in a moment. So let's add .vscode here and settings.json and just add them here. And if this opens up, simply click allow like this. And what you can do is press Command-Shift-P and then just reload window just so it kind of resets the entire thing. And go ahead and visit your source app homepage.tsx. And you can see that now I'm still getting my types here. But in case you were getting them as any, this will most likely fix it. But even if this worked for you initially, if the types were okay, I would still recommend having this because you will commit to this and then your colleagues who opened this project will have the same settings. And then they're not going to have any problems as well. So that's one troubleshooting issue that I had. And it was fixed immediately once I added this. And you can see that they also highly recommend committing this file so the colleagues can also get the same experience. All right. Now let's go back to our example. So we just learned how to use the RPC purely on the client. We learned how to use it purely on the server, which is this. But how about our prefetching solution? How about a hybrid between those two? How about leveraging the speed of server components, but leveraging the familiarity and the interactivity of client components right now? because we can't really do much with this. Sure, this loaded obviously faster than the client side, but what exactly do we do with this? Server components are not that interactive. We can't even add a hook here. We can't add on click, right? It's a bit more complicated to work with it. It seems like everything we gained with speed, we lost with interactivity. So that's why we are searching for a hybrid solution to get the best of both worlds. Let's do that now. Instead of actually expecting any data here, we're going to change this to be void hello.prefetch like this. Keep in mind that I'm not exactly sure if this needs to be asynchronous or not. And even more, if you don't make it asynchronous, it's not going to throw any warning. Unfortunately, I can't exactly tell you if it needs it or not. But in my project, I wrote async everywhere where I'm using prefetch, right? So I would recommend you do the same thing. And now what's happening is that we are not using tRPC on the server to get the result in a constant and use it. We are using it to populate our data cache on the server side and then allow the client component to use pretty much already loaded data, right? That's what we are trying to achieve. So what I'm going to do is inside of my homepage here, I'm going to create a little client.tsx and I will do use client here. I will import trpc from client like this. And I will export const page client component like this. Page client. And I will import page client from dot slash client like this. So now it should just say page client here. But if you take a look inside of your project here, if you do a hard refresh here... Oh, yeah. So nothing's going to happen yet. But the moment you add const this time inside of square brackets not curly brackets because we will be using trpc Hello use prefetch query like this My apologies Use suspense query, not prefetch query. Let's see. What exactly am I missing here? An argument for input. If you try now, page client says data.greeting. So first things first, you will already notice that this is always loaded. There is no possibility of this being null, right? Because this is a suspense query. We already started loading this on the server, but we are going to finalize the cache and everything on the client here. So right now you can see that we are using this as a client component. So we can now go ahead and add use effect and all of the other things, you know, which we were not able to do in the server component, but we are still leveraging the speed of server components. Because remember, in our first example, which was just the tRPC hello, and it was just use query, that was a bit slower. For a second, it was empty, right? And then it got populated. So when we combine these two, we get the best of both worlds. But this is not exactly a finished example. There are some things missing here, and that is the hydrate client, suspense, and the error boundary. So let's go ahead and add hydrate client from the RPC server. Let's add suspense from React. And let's go ahead and give it a fallback. Adding loading. Like this. So now if you do a hard refresh, I think this query is not really complicated enough to ever show the loading state. But now we do have a suspense which will show loading, right? We also need to add an error boundary here. And for that, we can use React error boundary. Let me just go ahead and see if this is the one I want to use. Yes, this one. So, let's use 5.0.0 React error boundary. Bun add React error at 5.0.0. Let's import it here. so import error boundary from react error boundary like this and let's give it a fallback error right now and nothing should change really but if you go ahead inside of your trpc routers app folder and if you go ahead and throw new trpc error let's see i think you can just select code bad request so this is also another super cool thing about trpc is that you have this strictly type errors right so you can just add not found if you want and it will be typed. So now you can see it says loading. It will attempt to load it a few times. And then I think after three or four retries, it will fall back to error. So this is the server first data pattern that we will be trying to achieve throughout this project. We are going to leverage the speed of server components to prefetch the data. And then we simply going to let the client component not have to fetch anything initially. It will just have the data ready inside. And for any loading states or errors, we're going to leverage the suspense and the error boundary. And yes, it will still have a loading state, but these loading states will be much, much shorter than in comparison if this was just, you know, use query and then we would have to destructure data and is loading here. And then we would do if is loading, right? We would have to do all of those things here. That's going to be much slower, especially noticeable on larger queries than this hybrid combination of both of this. And now there are a few, you know, tips and tricks that I just want to end the chapter with. Every time you use use suspense query, you have to have an equivalent prefetch here. Same thing is true if you do use suspense. I'm not sure if I can do it now. Yeah, I cannot do it right now, I think, because I'm missing the proper returns to create this into an infinite query, right? But if you are prefetching infinite, you also need to suspense infinite query, right? You can't exactly, you have to be careful about doing something correctly. Also, for example, if you have some input inside of here, if you suspend squarely Antonio2, but you prefetch text Antonio, that will not have its effect, right? Let me just remove the error that I throw here and remove the input from here. So if you refresh this, now it works because this is super simple, but it's actually not leveraging the prefetching that I was talking about, right? Because this doesn't make sense right now because all that these queries are doing are just returning back the input. But imagine this input was a video ID. We would first have to go to our database and load the video, right? So imagine if you have prefetched video ID 1 and then you suspend query video ID 2. That would completely lose its purpose. It would not be able to prefetch what it was supposed to prefetch, right? I hope that kind of made it clearer. We currently don't have any authenticated examples, so I can't really show you any other gotchas here. But basically, in case you use use suspense query for an authenticated procedure and you forgot to prefetch, you're going to get an error that the procedure has pulled back to client and it's unauthorized. That's because the cookies from the header, which were supposed to be prefetched here, were never passed inside of here. So yeah, it is kind of, you know, not exactly perfect, but there is, I mean, when I say not perfect, I mean, you have to be careful. It works, but you have to be careful with how you're doing it. But I will just pause the video for a second and I will try and find one GitHub discussion where they are finding a way to make this a bit more stricter. So the application itself will warn you if you're doing a mistake like that. So this is the discussion that I was just mentioning. Basically, this is one of the authors of Tanstack React query. And inside of here, they are introducing or discussing this exact issue, which I just mentioned, right? If you forgot to prefetch and then if you use useSuspense, it will not work, but it will also not throw any errors, right? So the main problem we are seeing with prefetching is code dislocation. I guess that's the proper name for it. So if you just call useSuspenseSquare in your component and having it work, it's very convenient. But without a compiler like Relay, it will not be possible to extract those data requirements and trigger prefetching somewhere else automatically. So what we're left with is duplicating what we do. Have a component called useSuspenseQuery with certain options and use the same option in your route loader to do prefetching. So that's what we are currently doing. For every useSuspenseQuery, we need an equivalent route loader to prefetch that query. Obviously, there are some issues with that. And here is a proposed solution. So they are working on query strict mode component, which I suppose you would wrap around your application or you will most likely just add it to, for example, TRPC client, right? You would most likely just add it inside of here. You would wrap the query strict mode and it will then warn you if you are missing a prefetch or if you have an unnecessary prefetch. So I think that's a very, very cool solution here. You can read more about it. So just go ahead and Google this. Even I left a comment here because I noticed that if I call prefetch infinite, but combine it with use suspense query, instead of calling use suspense infinite query, in that case, there is also an issue here. But this does not really fall under the same problem as they are trying to solve here. And I did get an answer for that. But yes, you do have to be careful about that. And I'm going to try and emphasize this throughout the project the more we build. But I hope that for now, it kind of is a bit more clearer than it was in the beginning, especially the first time you saw this diagram. I hope you now understand what I'm trying to do. So basically, we're trying to get the best of both worlds. We are trying to get the speed of server components and the interactivity of client components. We are going to exclusively use server components as route loaders and nothing more. So server components will just prefetch the data, and then client components will have that data ready to use, and it's going to be very, very fast for the end user. Great. So I think we successfully enabled TRPC. What we're going to do in the next chapter is we're going to combine our clerk authentication with TRPC, and we are slowly going to organize our routers and procedures in a nicer way. Great, great job. The goal for this chapter is to continue our TRPC configuration, which we started in the previous chapter. In this chapter, the goal is to enable transformers on TRPC, add auth to our TRPC context, add a proper protected procedure, and we will also implement rate limiting. And that will pretty much wrap up everything we need in regards to our TRCP configuration. And then we will be able to finally start adding some other entities to our database. We're going to start with a very simple entity later on called the categories. But let's not focus on that. Let's focus on what's in front of us. Let do the easy one first enabling transformers on a TRPC So let me just refresh my app to ensure everything loads nice and well And let go inside of TRPC right here Inside of my init method right here you will already see that I have this commented out transformer super JSON. So let's uncomment that and let's go ahead and import superjson from superjson. Now, keep in mind that we don't yet have a superjson. So let's go ahead and do a bun add superjson. And you're going to see which version I'm using in a second. So I'm using superjson at 2.2.2 like this. So let's go ahead and ensure that we have that here. That's great. Now let's go inside of query-client and let's do the same thing here. So import superJSON from superJSON, enable serialized data to use superJSON serialize and enable deserialized data to use superJSON.deserialize. And in the server.dsx, I think we don't have to do anything. Let me just confirm in the client here. Do we have any superJSON here, we do have superJSON here as well. And we also have an error here. So find the trpc client here, HTTP batch link. So I'm inside of client.tsx. Go ahead and uncomment this, remove this line. And now we also have to import superJSON from superJSON like this. And now we should no longer have any errors. I'm just going to search throughout my code to ensure that there are no errors anywhere. And nothing much should really change here. If I refresh now, I should be getting no errors at all. Great. So now that we have that, let's go ahead and see what's next. What's next is to add auth to trpc context. So right now, let's go ahead and go in one of our routers here. If I go ahead, for example, and add on the log here, hello world. And if I refresh here, we should be able to see hello world right here on our server side. So now I'm going to try and console log the currently logged in user. At the moment, we can't really do that. Well, technically, we should be able to do it if we just destructure user ID from await auth, which you can import from clerknext.js server. And if you mark this query as an asynchronous query, And then if you just add user ID in the console log and refresh, you can see that my user ID is null here. But if I go ahead and log in now with my email here, let's continue. Let's get redirected back. Okay, let me try again. There we go. You can see that now I have a hello world and I have my user ID. So that's great. Some of you might already have some ideas how we can fetch the user from the database now and get the actual database record. But it doesn't really make sense to do this every single time. Because what we have to do then, if we want this to be a protected procedure, is check if there is no user ID and then throw new TRPC error like this. and give it a code of this would be unauthorized or unauthenticated, right? So it doesn't make sense to do that every single time. So for now, that's not something we're going to do. We can leave this. We can even remove the async. We don't need any of this. Let's create a reusable way of doing this. In order to do that, we're going to go back inside of our init method here, and we're going to revisit our createTRPC context. If you want to, you can learn more about the context, And you can see how they give you an example of using next out to get session inside of that context. I had a talk with one of the maintainers and creators of TRPC. And I asked them, you know, how exactly, what exactly should I do in the context? And they gave me a tip that I should not really do any database queries inside of here. So the context is something that will be available for every single API call that you do. So we want to keep it as light as possible. In order for us to do that, the perfect method is the await alth method from clerk next JS server. And then simply return clerk user ID, user ID. The reason we want to use this one is because the out helper returns the out object of the currently active user. Only works server side, such as server components, route handler. Okay, I was hoping they're going to give me a description of what I was hoping for here. Basically, this doesn't do, as far as I know, any fetch and any fetches. It simply destructures the current JVT token or the current session, right? Which makes it a very lightweight method to call as opposed to, for example, current user from clerknext.js, which returns the backend user object of the currently active user. So it calls the fetch, as you can see right here. It calls this method, whereas out does not do that right here. All right. So now that we have this, I also return this in this context under clerk user ID. So the reason I return it under clerk user ID and not just user ID is because I want to be very explicit, right? My user ID right here inside of this context, whenever I see user ID, I will explicitly mean on this, my database user ID. But what I have here is my clerk user ID. So I want to make sure that all of my procedures will know that whatever I'm doing in the context is not the database user ID. It's just the clerk user ID. All right. So we have the context here. And now what we have to do is we have to create a proper type for our context. So export type context will be awaited return type of type of create the RPC context like this. Now, when you hover over context, you can see that we have a potential clerk user ID inside. And now we're going to add that inside of this init.rpc.context, pass in the context type like this, and then pass in the create like that. There we go. And now what we are going to do is the following. Let me just see. Can I now go inside of my app here? We have options is context. Okay, I think that inside of options here, I think I can do console log now options. Sorry, options. There we go. Context.clerkUserID like that. So that's what I wanted to see. So let's go ahead and just add from context, options, context, clerkUserID. I mean, since the moment we are getting proper types here, it probably means that it's working just fine. So let me go ahead and refresh. And there we go. From context, and we have the proper logged in user ID here. Great. So that's one step better. We don't have to repeat our await alf method, right? We only did it once. But how about we create another type of procedure, which we can then use every time we know the user has to be authorized for that call. In order to do that, we can export const protected procedure. And what we're going to do is we're going to extend the base procedure like this. And then we're going to use a middleware inside. So let's use asynchronous function is outfit like this. And it's going to have options inside. Let's go ahead and let's destructure context from the options. If we have a missing context clerk user ID, we're simply going to throw new trpc error. Make sure you import this from TRPC server, from the package, not our instance of TRPC server. And passing the code unauthorized like this. And then what we can do inside of here is, well, we can, you know, end the call here. For example, we can just return options.next like this. And we can just spread the context inside. and user ID will be context clerk user ID. Well, actually we didn't have to do that because, okay, this doesn't make sense now, but I will explain why I'm doing it like this in a second. But already nothing should really change right now. If you go inside of your router here, for example, and replace the base procedure with protected procedure now and remove the base procedure here. If you refresh, you shouldn't get any errors. Everything should work fine. But if I log out and refresh, now I should be getting 401. As you can see right here, something is going on. Something is wrong. That is because I explicitly used protected procedure. But if I change it back to base procedure and refresh, there we go. No error at all. I just simply get a console log from context to be null. Great. But let's go ahead and do one step further, which is to actually always add the database record, which we have because remember we have our webhook running. I'm running bun run dev all, which means that I have a proper webhook here, which means that all of my users are synchronized. Let's just confirm that. If I go to Bunix DrizzleKit Studio here, local drizzle.studio, and if I go inside of my users, I see my user right here. So I want this object to also be available in my protected procedure. And that's what we're going to do now. So let's go inside of the init here. And first of all, we throw the error if this doesn't exist. And now let's go ahead and let's get from await database, which we can import from alias slash database like this. So we're going to do database.select from users, which we can import from database schema. Where equals users.id context clerk user ID. And I made a little mistake here. We're going to fix it in a moment. First of all.\nwe need to import equals from drizzle.org. And second of all, this query will not work. Why? Well, because users.id will never equal clerk user ID. Instead, we will have the field user.clerk ID. There we go, like that. And drizzle will always return an array of items, right? So even if you do limit to one, which is completely okay to expect here, it will always return back an array of items, right? You can see that it's always an array. The reason they chose that is because that's how SQL works. SQL will always return an array. So no matter how you want to do it, you can limit it to one, but just make sure you know to... Of course, if this has changed in the future, don't worry. Just hover over data and confirm. Is this an array? If it has a little array here, that means data is an array. So what I like to do is I just destructure the first item from here, the user, like that. And then what I do is I just check again. If there is no user, in that case, I'm going to throw a new error, just the same one as above, which basically means, okay, you are logged in, but for some reason, I cannot find you in the database. And I cannot let you continue further with this request. All right. And then what we can do here is we can spread the context and just simply pass our user here like this. There we go. So now inside of our app here, I'm going to go ahead and use the protected procedure here this time. And now I'm going to check database user to be our options context dot user, as you can see here. So now I can get that user. Let's ensure that you're logged in. So this query does not fail. Let me refresh. Let me scroll all the way down here. And there we go. My database user is loaded here. So that's how we are going to use the protected procedure, right? We could have done this entire database call inside of the context. But remember, the context will run every single time for every single procedure that we do, regardless if we want that to be a public procedure or a private procedure. So that's why it's best to just destructure the JVT token, something that doesn't do any fetch calls, which in this case is a wait out. In case you yourself are planning on changing this to next out, this would be get session for you. Basically something that's not doing an API call. And then in your protected procedure, you can go ahead and do the API call. And here you can see exactly what we previously did, right? We created a type of context and we passed that here. And then we were able to access options.context inside of a procedure. Excellent. So we finished that. Let's see what else do we have to do? What we have to do next is we have to add rate limiting. So that's another interesting thing that we have to add. And it's a great introduction to Upstash because we will be using Upstash in different places in this project. The first place will be an easy introduction to Upstash and a good practice for us to play more with this TRPC middlewares. We're first going to implement rate limiting so no one can spam our TRPC endpoints. And then later on, we're going to use Upstash for background jobs for long-running tasks in which we can completely rely on AppStash for retrying and keeping them in order. Those are going to be long-running AI tasks, which more often than not will time out in any non-edge or serverless function, right? They can take up to a minute or something like that. Or even, you know, ours, AppStash workflow will be a perfect solution for that. So that's why I want us to introduce to Redis very early on. So let's go ahead and finish that last thing. Because right now, no matter how many times I refresh my app here, it will always load. I can never spam it enough. So let's go ahead to opstash.com and let's go ahead and create an account. Once you're in here, go ahead and create a database. I'm going to call this new tube. For the region, I really don't know which one is best for you. You can just select the first one. And let's go ahead and select next, select the free plan, right? And let's go ahead and see everything that we need right now. So what I'm going to do is I'm going to click here and I'm going to click on rate limit analytics. And maybe I have to go to docs here. Let's go ahead and go together to learn how to set this up. So examples, next JS. Okay. This leads me to this will lead me to the GitHub file. Let me see if we can do it easier like this. So we can start by just installing the packages which we need starting with the upstash redis package. So let's go inside here and let's do bun add at upstash slash redis at 1.34.3. That is the current latest version at the time of this tutorial. So I'm going to add that. And then we're also going to do bun add at upstash slash rate limit. And that is 2.0.5 like this. So let me quickly show you my package.json. There we go. So upstash rate limit and upstash reddit redis my apologies okay now that we have those two we have to add our environment variables so you can find them here in your you can just click you know on your project here click on your database and you should be able to see your uh environment variables right here so you can copy the uh upstash redis url and upstash redis token right this you can just click copy, go inside of your .environment.local and add them here. So, upstash Redis URL and upstash Redis REST token like this. Again, do not share this with anyone. Now, let's go ahead and let's go inside of init.ts. And what we're going to do here is we're going to import both Redis and rate limit. So let's start by importing redis from add ups dash redis and let's import rate limit from add ups dash rate limit like this. And now what we are going to do is we are simply going to separate some space between base procedure and the protected procedure and let's add our Redis here to be new Redis with the URL to be process.environment and simply copy the upstash Redis REST URL. And we're going to have a token to be process.environment and then copy your upstash Redis REST token. Copying is almost always safer than right typing it out because I've made so many typos when I did that. And, you know, let's do this in a proper way. let's go ahead and create a lib called redis.ts and let's do that instead so new redis from upstairs redis and then we can export this like this so we don't have to define it here for no reason and we can do the same thing inside of our lib here for rate limit.ts so rate limit.ts like that let's import rate limit from upstairs rate limit. Let's import Redis from dot slash Redis. And then let's do export const rate limit to be new rate limit. Passing Redis as the first argument. And then the limiter inside of here will basically be your definition of what is too many requests. For example, 10 requests within 10 seconds will cause a timeout, or you can do 50 requests within 10 seconds will cause a timeout request. I believe that in docs here, if we search for sliding window, you can perhaps learn a bit more about how it works. Let's see sliding window, traffic protection right here. There we go. So inside of here, you can learn a bit more about this limiter and how it works. For example, 10 10 requests within 10 seconds. And you can block this by anything. If you have an IP address, you can block by that. If you have a user agent, you can block by that. I'm going to show you a very simple way to block by the currently logged in user ID. So let's go ahead and do that. Inside of the protected procedure, what we're going to do is the following. Once we ensure that we have this user inside of our database. What we're going to do is we're going to check if we will allow this user from making any more requests by using await rate limit, which you can import from lib rate limit. And yeah, we don't need this to import. I think it makes no sense to have them defined inside of this file. So await rate limit dot limit and pass in the user dot ID like this. And in case success fails, we are simply going to throw new TRPC error code to many requests like this. And now if you go ahead and try out and refresh a lot of times, so still not failing, not failing. And now, as you can see, I have TRPC error to many requests. And I believe that if you go inside of here, rate limit analytics, and we select new tube database here, I think that you should be, maybe not immediately, but I think you should be starting to see some requests here in a moment. Or perhaps this will only appear in production. Great. So you've learned how to build all of that. If you try one more time, then it goes back to normal, of course. Depending on, you know, your development style, you might really, really spam this refresh. So feel free to increase this to a hundred or, you know, you can completely just disable it. If you think this will cause more problems than not, you can just console it out like this And yeah I think that pretty much says everything we need to do in this chapter right Let see So let me get my pen here So we added the transformers to the RPC We added out to the context. We added out procedure and we added rate limiting. And we also got familiar with opstash and redis. Great. So, now let me do one more thing. And the only reason I'm doing this right now is because I did it in my project and I saw it in a couple of other projects, but I'm not exactly sure how much this benefits you. But inside of client.tsx, when it comes to tRPC create client, inside of this HTTP batch link after URL, what I did was I added asynchronous headers right here. I define the headers to be new headers like this. And then I did headers.set x-trpc-source to be next.js-react. And I return back the headers. Now, I've found this to have some logging improvements, but not more than that. I just want to stay true to my source code. If this ends up causing any problems for you, you can, of course, remove it because we just tested everything still works fine with or without it. But I think even the T3 stack has this line of code. So I think it will be helpful for logging and debugging. Right. So if you want to, you can add that. And I think that this new headers is native. It's a fetch API, yes, MDN reference. So there was no import for the headers. Great. So I think that wraps up our chapter right now. What we're going to do next is we're going to go ahead and add categories to our application. So we're going to load that carousal right here. You're going to see a picture in the next chapter. And we are going to organize our router a bit because right now we only have this entry point, underscore app, and we just started writing procedures here. And you can probably imagine that can be quite a mess if we just start putting everything here. So we are going to restructure this just a little bit. And yeah, that's going to be a good start into going back to our previous lesson, which was the hydration, right? So we are going to come back to this prefetching thing, but this time not with dummy data, but with actual categories which we need to prefetch from our database. Great, great job. In this chapter, the goal is to create video categories. This is the component which we are going to create. And we're going to put it right here below the search page and just above where our videos are going to be. And this is what it's going to look like. We're going to use the carousal component from ShatZNUI, so it's going to look pretty smooth. We're going to start by creating the categories schema. After that, we're going to push those changes to our database. Then we're going to create a script to help us seed the categories, because categories are not something that users are going to create. It's more of a super admin thing that will simply be set in the project for users to then choose from. And then we're going to utilize our knowledge from chapter six about prefetching data. In this case, prefetching categories in a server component, And then we're going to use suspend square with them inside of a client component. So we're finally going to leverage that knowledge in a real world example. And this is a fairly simple entity for us to do because there aren't really any create, read, update methods on here. And one more thing that we are going to do here before we prefetch is organize the RPC routers, right? That's one more thing we're going to do. All right, let's start by creating our seed script. So I'm going to go inside of the source folder, and I will create a new scripts folder inside. And inside of here, I'm going to add seed categories dot TS. What I'm going to do now is I'm going to add a to-do here, create a script to seed categories. So let's just have this ready. We can't do it yet because first we have to go inside of database schema.ts. So let's go ahead and export const categories to be PG table categories. We're going to have the primary ID to be UUID. So let's add that. Each category will have a name, which will be required and also unique. So no two categories will have the same name. Now, the description, well, it will depend on how you want to handle these categories. As you can see, you know, in here, the descriptions are really not going to be used for anything, but they might be useful for you if you ever decide to create a dedicated screen for each category. So then you can have a little description saying, you know, for comedy, you can have a unique thing saying like the funniest videos or something like that. So if you want to, you can add a optional description here like that. So not null. So it's just an optional description. And let's just copy our two created, add, and updated methods here. And one thing that I'm going to do here is I'm going to add an index for the name of the category in case we want to query by the name. So I'm going to get T and I will return unique index here, name, index. and I will do on t.name. There we go. So now we have categories here. Perfect. So what we have to do now is we have to go inside of our terminal and we have to run Bunix drizzle get push like this. And once you run this, you should get a success message that everything was pushed fine. And then we're simply going to check our drizzle studio to confirm that this schema has truly been modified. So it says no changes detected. Oh, my apologies. I am in an incorrect project. Let me go ahead and go back here. Ignore that. For you, everything probably went well. What happened to me was I was in a different terminal. So newtube project, Bunnix DrizzleKit push. I will just run it again. And now I expect everything to work fine. There we go. So changes applied, pulling schema from the database. I noticed something was wrong and that's what it was. So now let's go ahead and do Bunnix DrizzleKit Studio. And let's go to local.drizzle.studio just to confirm that we now have categories in here with ID, name, description, created at, and updated at. Great. Now we can go back inside of our seed categories right here. And let's go ahead and define our categories. So const category names are going to be the following. Cars and vehicles. Comedy. And well, it makes no sense for you to watch me type this out. I'm just going to copy all of the categories which I'm going to have. And you can pause the screen and add them if you wish. It really doesn't matter, right? The more you add them, the better the site is going to look like. But these are the ones I was able to found in the YouTube Studio category select input. So it only allowed these categories right here. So I decided to only add them as well. Great. Now let's do an asynchronous function main. Let's add a console log here, seeding categories, like this. And now let's open a try and catch block. Let's resolve the catch block first. So we're going to add an error here, error seeding categories, like this. Now let's add a little space here. I have a typo here. And of course, let's do process.exit1 because we will be running this in a script. And make sure that you execute the main method here outside of itself, of course. And in the try method, what we are going to do is we are going to do category names.net and get the individual name and return an object here. And we are going to reuse the name and we are going to generate the description. by saying videos related to, and then just name to lowercase. Like this. So basically, we're just going to create a very generic description for every single category name. So it's going to say, you know, videos related to sports, videos related to travel and events. So we are creating the values. And what we have to do now is await database, which you will import from at slash database, dot insert into categories, which we now have from our schema as well, and pass in the values, which are values like this. And after that, console log categories seeded successfully like this. Now, in order to run this script, you are going to have to use something that supports this kind of imports, right? That's why in the beginning of our tutorial, I told you that I highly prefer if you used Bun for this because Bun lets us easily run TypeScript scripts with ES6 imports. So I'm going to show you first how it looks like in Bun. So let me go ahead and try running this. Let's go inside of the terminal here. and I will do bun run, actually not run, just bun scripts, actually source scripts, seed categories. There we go. Seeding categories and then categories with my typos seeded successfully. So if I now go ahead and open the DrizzleKit Studio again, and if I refresh the entire thing now, you should see 14 categories here. People and blogs, film animation, everything here. and you see that you have the relevant description as well. Right? Now I not sure exactly what happens if you try to do NPM My apologies not NPM If you try to do node source scripts seed categories there we go you get errors right Because we are using import which is not something that you can use. I'm not even sure what's the error. Can it use import statement outside a module, right? It's just things like this that make me highly prefer using bun in these tutorials. if you really really want to use want to use node you can do it by adding cons this to be required and honestly I'm not even sure if this can use an alias let me try this I'm not even sure how this works I this is so funny I have not used them in such a long time I'm going to pause the video and try and give you a solution if you really want to use node. So I think that the solution might be to use requires like this. I think. So I have to escape this and go inside. No. Or maybe just here. See, this is why I really, really just prefer using bun. Let's just try. If I run node cannot find module okay yeah i give up i'm really not gonna try and solve this because bun just makes it so much easier so please just just use bun for this it's gonna be 10 times easier for you to continue with the tutorial all right so now that we have our categories set what we're going to do is we're going to create our categories procedures, right? So let's go ahead and do this. Let's go inside of source, inside of modules, and let's create a new folder called categories like this. And inside, let's create a server folder and then a procedures.ts like this. And inside of here, what we're going to do is we're going to import database from database. We're going to import categories from the database schema. And we're going to import base procedure and create trpc router from trpc init. And then we can safely export const categories router to be create trpc router and passing get many. And this will use a base procedure, meaning it's going to be public to everyone. Inside of this query, we're going to have an asynchronous method. And all we're going to do is we're going to get the data using await database select from categories. Like this. And we return back the data. That's it. That's our categories router. One issue, though, is that we are not really using it anywhere. So let's go ahead and let's refactor our TRPC routers here. So go inside of TRPC routers. And what you can do now is you can remove the hello protected procedure. You can remove this from here. You can remove Zod as well. So you are only going to need the create TRPC router. And above, you're going to import categories router from modules, categories, server procedures, and then passing categories here to the categories router like this. There we go. Now, obviously, we're going to have some errors. Instead of source, app folder, home, and instead of here, we have page, right? Hello no longer exists. So how about we modify this to load our new categories? So let's prefetch the categories. Let's see. Categories, my apologies. Categories get many. Prefetch like this. And let's go inside of client here and let's do the same thing. Categories get many and nothing is being passed here. And what we are going to do here is simply JSON stringify data like this. So if you now go here and refresh and of course have your app running on run dev all and reload your application, you should now see the categories inside of a JSON format right here. There we go. Great. So now what we have to do, I believe, is inside of our Chapter 8. So let's keep track. We have successfully created the categories schema. We pushed the changes to the database. We have seeded the categories. And we have organized our TRPC routers. And now we have to properly pre... We are already prefetching them, but not in their proper place, right? So now we have to create the categories component. So let's go ahead and fully focus on this now. We are not going to be needing this protected anymore. We just use this for demonstration. Let's remove that. We are also not going to need the client.dsx anymore. Let's remove that. And let's exclusively focus on home page.dsx. inside of here, let's remove the page client and let's also remove the suspenses and the error boundaries as well. We are only going to do void trpc categories get many dot prefetch. And here's an important thing. When Next.js or Vercel tries to build this part of our app, it will think that this is a static application because it has no idea that this is actually prefetching something. So what you have to do, and don't worry, you're only going to have to do this in page.tsx, and we're going to keep track of every single page.tsx in our app, which does prefetching. We're going to be careful. What you have to do is export const dynamic to be force dynamic like this. Otherwise, you're going to have build errors if you don't do that in places where you prefetch. And now inside of here, what we are going to do is we are very simply going to pass the home view. Now we don't have the home view yet, but we are going to create it. And I'm also going to kind of explain to you the places where I prefetch and the places where I call the use suspense query, because I will make some decisions about where I will do that and where I will absolutely not do that. And also to answer, you know, where did the suspense and the error boundary go? We are also going to take care of that. But let's finish doing this. How about we don't call this home. Instead, we just call it a page. I prefer using arrow functions, export default. Don't forget the default export when it comes to pages. Let's create an interface page props here. And we're using search barams here, which means we have a promise like this and an optional category ID. So this is not required to exist. Only if the user selected the category ID will it exist. And let's go ahead and destructure that here. Page props, search params. So not params, but search params like this. And then inside of here, you can destructure the category ID from await search params. And in order to use await, we also have to ensure that this component is asynchronous. And then we can go ahead and pass in the category ID. There we go. So we are leveraging the server component to await our search barrens. And we are also leveraging it to fetch all the categories, to prefetch the categories. And now we're going to create a new module here. Actually, we already have it called Home. We have the Home layout and we have the components here. So now what we are going to add is another type of UI components called views. And inside of the view, we are going to go ahead and we are going to create a home view.dsx. The home view component will have an interface, home view props, which will accept the category ID, which will be already destructured here from the promise. So no need to do that. Let's export const home view here. Pass in the category ID. from home view crops. And inside of here, what we are going to do is we are going to return a div. And we are going to give this div a class name of maximum width of 2,400 pixels. I'm going to explain why we are doing this in a moment. Let's add MX auto, margin bottom of 10, PX of 4, padding top of 2.5, flex, flex column, and gap Y6, like this. Basically, what we are doing is the following. I think if I add border and background red 500, it might be a bit more clearer. So let me just go ahead and do this. And let me go use the home view inside of my actual app folder home page. So I'm going to import the home view from modules views home view. I will refresh here. And there we go. You can see my home view now. But you can see that at one point, it will stop expanding its width, right? So if someone is looking at this at an ultra wide screen, we're not going to keep expanding the videos infinitely, right? We're just going to limit to how wide we are going to support our category carousal and our video grid. So that's what this does. It limits to how wide we are going to support that. So we can now remove the background color red and the border. We don't need any of those. And now, instead of the home view, we are still not fetching anything. Instead, what we're going to do is we're going to render the categories section and pass in the category ID to be category ID. like this. So the last type of component for me to introduce to you are the sections like this. And inside of here, simply create category section dot DSX. And the section will finally be an actual client component. I will just copy the interface from home view props because it's exactly the same and I will simply rename it to category section props. Let's export const category section. My apologies, not category section. Categories, multiple. I want to have proper names. Like this, categories. And inside of here what we are going to do is we are going to fetch our categories like this So categories PRPC let import from client Make sure you're importing from client here, but in page, you have to import from server, right? So in the client here, we are going to access the cache, which we have. My apologies. Get many. Get suspense. Use suspense query. We are now immediately going to access the cache, which we have, thanks to this prefetch in our server component here. So now that we have that, let's go ahead and return a div JSON stringify categories. So the same thing we previously did, but this time in a structure that we are going to follow throughout our project. So let's go ahead and add this and structure the category ID. All right, and let's go ahead and import the categories section from . . . sections category section. Great, so now if you go ahead and refresh, nothing much should change. Everything is pretty much still the same, but this is my general UI structure. So, inside of my app router, I'm not going to do any UI elements. As you can see, I will immediately import the home layout from the modules home. For the page, I will always import a single home view, but I will hydrate the client here. The reason I'm choosing to hydrate the client here and not inside of the home view is because I don't want to forget it, right? I don't want to accidentally forget that I have to hydrate this client. So wherever I am doing a prefetch is where I'm going to add hydrate client. It's this kind of consistency that gives me a peace of mind that I didn't forget to do any of those. If I prefetch, I have to hydrate and I also have to use the proper use suspense query. This is giving me a peace of mind that I will always do the same thing every single time. Great. So that's what my pages are going to be for. My server components will be used to prefetch things, to hydrate the client and to render its respective home view. Now the home view component itself will be purely decorational. All it's going to do is going to split the screen into sections And then the section will finally be a client component, which is going to leverage the prefetching from my server component. That's how that is going to work. The reason I am separating them into sections here is because each of my sections will have their own suspense and error boundaries, which means that I'm going to have categories sections here, but below I will have videos section. So both of these will be able to load independently of one another. And if one crashes, it's going to have a nice encapsulation as opposed to my entire view crashing, right? That's what I'm trying to achieve here. I'm trying to find a balance between two granular of suspending and also, you know, two grandiose of suspending as, for example, suspending the entire home view, right? I'm rather going to go a bit deeper into sections. I think this gives quite a perfect balance between not too granular, but not too grand either. All right. So we now have the categories here. But something that we don't have at the moment is any loading screens. So you can choose how you want to do this. So you might think, okay, so I have to wrap this inside of suspense, right? Because my categories section is using use suspense query. We've learned that we have to wrap those inside, but I'm actually not going to do that. Now, I'm not sure how good of a practice this is, but I really like it this way. This is what I'm going to do instead. I'm not going to export this, and I'm going to rename this to category section suspense. And then in here, I'm going to export const categories section like this. And I'm going to explain why I chose this. It's pretty much the same reason why I chose to use hydrate client in the place where I'm doing prefetch. I'm choosing to have inside of the same component, I'm going to have the suspense in the error boundaries. So I never forget that I have to wrap whatever, whenever I'm using use suspense query, I never want to forget about wrapping them in these two. So that's why I simply decided, okay, every single place where I use, use suspense something, I'm going to do the actual suspense and error wrapping inside of the very same component. And that way I will never forget about it. Right. So that's my solution of doing it. If you have any better practices, feel free to introduce them. But keep in mind that I will be using this throughout the entire tutorial. React error boundary like this. And let's go ahead and add some fallbacks here. So add a paragraph loading like this and a fallback here, error like this. And then simply render the categories section suspense. But we also have to pass in the props. So let's just copy this. Yeah, this is the one annoying thing we have to do. We're going to have to do double prop passing here. Yes. But other than that, it's pretty much okay. All right. So now we have the categories section here and nothing has changed here because what we're importing here is this, the suspense with the error boundary. That's why I'm only exporting this and I have renamed this one to be categories section suspense. So now you can see that I have a proper loading suspense happening here. And if for whatever reason, inside of my categories router here, I were to, you know, throw new trpc error here with code bad request, I would get the respective error boundary. So it's loading, it's still loading. So only after a couple of retries will it fall back to my error boundary. There we go. Error boundary activated. Great. So I'm now going to remove this error here. So now I'm going to go ahead and what we're going to do now is finally develop this nice UI right here. So let's go ahead and do that. The component that will be responsible for this will be called a filter carousal. And it's not necessarily going to be used just for categories. It can be used for something else. For that reason, I will not put it inside of any module. I will just put it inside of my components. but I'm not going to put it inside of the UI folder purely for the reason that ShatCN uses this folder. So I will reserve the UI folder for whatever ShatCN command line interface decides to put inside. But I will use the components folder directly like this outside of the UI. And I will call this filter-carousal.tsx. So as you can see, filter-carousal is inside of components, but outside of my UI folder. This will be a client component. And let's go ahead and do the following. Let's first of all, create an interface here. I'm just going to copy it so you don't have to see me typing this thing out. So go ahead and pause the screen. The value can be optional string or null. Is loading can be optional. On select will choose a specific value. And the data has to be in this kind of format, value or label like this. And the data actually has to be required. Right. Actually, yeah, let's make the data always be required because we do allow an empty array. So that makes sense. Great. Let's export const filter carousal here. We now have to assign the props like that. And inside of here, we can now destructure value on select data and is loading like that. Now, what we have to do is we have to use the carousal elements from ShatCN UI. So let's add all the imports necessary for that. So that's the carousal, carousal API, carousal content, carousal item, carousal next, and carousal previews, all coming from components UI carousal. Double check that you have that component. Great. So now that you have this, we can go ahead and just build a basic look of this. So let's go ahead and do the following. I'm going to go ahead and return a div with the class name relative and the full width. And now what I'm going to do is I'm going to add the carousal like this. and I'm going to go ahead and give this an options of align start, and I'm going to give it drag free to be true. I will also give this a class name of full width and px12, like this. I'm then going to add carousal content here, and I'm going to give this a class name of minus ml minus three. If you're wondering why minus ML minus three, that is because this is how you add certain padding between the items. It's a trick from the Shatzian UI documentation. If you want to, you can have it opened. But yeah, the Shatzian in the background for the carousal, I believe, uses Mblock carousal react. And basically, that's how you add padding between items in each carousal here, right? so what we are going to do is the following how about we just add one carousal item here which will simply use our batch component from dot slash ui badge or slash components ui badge however you want to import it because we are in the components folder so we are much closer to the ui folder than usual so whatever whichever one you prefer and we're simply going to write all inside like this. And I'm going to pause here so that I can go back inside of my source modules here inside of home sections categories section. And inside of here, all I'm going to do is I'm just going to return filter carousal from components filter carousal. And I'm going to pass in the value to be category ID and the data to be data. My apologies. Yeah, we can call it data. We can call it categories, however you prefer. Let's call it categories. Yeah, the issue is that we actually have to give it a specific type because data expects a value and a label, right? So let's go over our categories, get the individual visit name like this, and ID, and return an immediate object with value to be category actually\nJust ID and label to be named like this. And now you can pass in data. There we go. Or you could have also, you know, passed this as category. And then you could have used category.id and.name, whichever one you prefer. Let me refresh now. And there we go. You can see a small all badge right here. So I just wanted us to render this component so that when we develop it, we can see the changes. So obviously, it doesn't really look perfect yet, but we are going to modify it. Let's go ahead and let's give this badge, which renders the all text, a dynamic variant. If the value is currently null, we're going to use default. Otherwise, we're going to use secondary. And let's also give it a class name of rounded large, px of three, py of one, cursor of pointer, white space, no wrap, and text small. So we are going to leverage the fact that if no value has been passed, and we are just going to assume that that means that the badge all is selected, right? Great. So now, as you can see, this is what that looks like. So now let's go ahead and do the following. Let's go ahead. And while we are inside of the carousal content, I want to check if we are not loading and if we have data. Actually, if we are not loading, in that case, just iterate over the data like this and render the carousal item. And inside of here, render the batch. Let's go ahead and give each carousal item a key of item dot value. And inside of each badge, item.label. So now if I refresh, looks like I'm still not getting anything here. Let's see. Instead of my categories section, let me check. What is my data here? Am I getting all the data necessary here? It looks like I am. This seems to be my data. So we definitely have this. But for some reason, it seems to not be loading. so is loading all right so if not is loading and and data.map am i using something wrong here if i just go like this will that work uh no it still acts as if my data does not exist so maybe it's not a problem in it's not yeah the data is available here as well so perhaps I'm using the UI incorrectly. Inside of my carousal content, we are rendering carousal items here and the badge here. Let's see. Could it be that I'm simply missing the classes? So, class name pl3basisauto. Is it that? It's not that. Very interesting. Okay. So, pl3basisauto, carousal item, badge, item label, all of these things should work, but for some reason, they don't appear to be working. All right, so I was debugging a bit using inspect element, and I did find the rest of our items. They're right here. They're pushed all the way to this other side, it seems. So let's go ahead. We're obviously doing something incorrectly here. I think that for this carousel item, I'm missing the same class name here. PL3BasisAuto. There we go. So I was missing that class name, which accidentally seemed to have pushed all the elements in this corner here. Great. So we can now see all of our categories. And now let's go ahead and give some additional styles here. So the badge will do the same thing. It will have a dynamic variant. If value is equal to item.value, we're going to use default. Otherwise, we're going to use secondary like this. And let's give the badge itself a class name, roundedLG, basically the exact same thing as above. So let's just use this. There we go. There we go. Now these are looking much better. And you can see how we can drag them. But there are a couple of things I don't like. The first thing is I'd like to have some arrows. The second thing is I don't like the cutoff here. So let's go ahead and resolve that as well. We can do that quite easily by going outside of the carousal content and simply add carousal previous and carousal next. So simply add them, give this one a class name. So for the previous, give it the class name of left zero and Z index of 20. And this one, right zero and Z index of 20. And there we go. You now have nice little arrows that you can use as well. One thing that's missing are the cutoffs. So I really don't like how the cutoff is looking like. So I found a very simple solution, thanks to AI, which is just to create like some fades. So let me remove the console log here. Inside of a relative full. I'm just going to add a comment left fade so you know what this is for. And this will be a self-closing div. And all it's going to do is have a class name, but we're going to have it have dynamic classes. So for that, we're going to utilize our lib utils, which we got when we installed the ShatCN UI. I don't think we've used it at any point so far, but what this is good at is making dynamic Tailwind classes. Yes, you could just use normal JavaScript here, but trust me, this makes it way easier, especially when you're about to have a bunch of classes, a bunch of options, a bunch of variants, right? You will eventually give in and start using this. And most importantly, it will properly merge Tailwind classes. So you might think you're doing something in a simple JavaScript way, and then your Tailwind classes are not gonna behave as you expect. So it's always safer to use this, in my opinion, when you need dynamic tailwind classes. The way it works is very simply. It accepts an infinite amount of params. And you can write them either statically like this. Absolute left 12, top 0, bottom 0, width of 12, Z index of 10, background gradient to right, from white to transparent, pointer events none, and then add a comma. And again, you can continue background red, blah, blah, blah, or you can make a dynamic one. And well, we don't really have, basically, I want to make something here. So this is what I'm going to do. If false, hidden. This false will be a variable in a moment. We don't have it yet. But now you can see how we have a nice fade here. But there is an issue is that I don't want this to be hidden already, right? I want it to be active in this case, but I don't want it to be active in the beginning because it's hiding my first element here. But basically, this is what we want to do. We have left fade here. Let's go to the end of our div here and let's add right fade. And instead of left 12, this will be right 12 like this. And this will be gradient 2L like this. And this will also be false. And now you're going to have a nice little fade at the both of these things here. So what we have to do now is we have to enable, well, let's go ahead and call this proper API for the carousal, right? Because when something is clicked, I want that to be sent to my, well, to the outer component, right? And from Shatsy and documentation, what you can do is you can enable Carousal API and expose it. And then you're going to have the ability to programmatically control this Carousal, which is quite useful. So let's go ahead and import useState from React here. And let's pass in a type of Carousal API. Do we already have it imported? We do. Great. So Carousal API here. The current value will be zero, as in the starting position. And we're going to have the count of total items. Let's add useEffect. I'm not a big fan of using useEffect ever, but sometimes you just have to. So make sure you've imported useEffect from React here. So if we did not load the API for the carousal, there's nothing we can do. So we can just break this method. Otherwise, let's call setCountAPI.scrollSnapList.length, execute it, and then call the length. And let's do setCurrent to be APISelectedScrollSnap plus one. API on select, set current, API selected scroll snap plus one. So now we have total control over how many items we have and what is the current position. And then we can properly hide this fade if we are on the first element or the last element, right? So yeah, it's a bit of an effort, but it makes the project look much better, right? It's the details that matter. Let's go inside of the carousel here and let's add set API to set API. And I think that's all we have to do for this. And then instead of false, we can check if current is equal to one. In that case, hide this. So now you can see no fade in the beginning, but when I move, it starts to fade. So it doesn't have the ugly cutoff. Perfect. And for the last one, instead of hard-coded false here, we're going to do if current is equal to count, right? So if we reach the end of our line, there we go. No fade here. But if we are not, it's going to show it like there are more things here. Great. So we have our basic carousal here. So what I'm wondering now is this. This shouldn't be if value is equal to null. this should just be if value does not exist and then all will be selected by default. That why it wasn selected because it wasn explicitly null All right so we have that And we can now send our data to this filter carousel But what's missing is a nice loading state. So how about we leverage this filter carousel and give it a prop, which we already have is loading. And let's go ahead and kind of make this a nicer experience. So we are only going to render the data If we are not loading. So if exclamation point is loading. Only then render the data. Otherwise. We're going to go ahead and do the following. If is loading. In that case. You're going to render an array. Actually. So yeah. Don't return any parentheses. Just collapse the element like this. We're going to create an array. From 14 elements. Because that's how much we have in the database currently. So skip the first argument and just get the index. And then we're going to mock the carousal items here. So give this a key of index. Give it a class name of pl3 and basis auto. And inside of here, we're going to use the skeleton from dot slash uiskeleton or from components uiskeleton. Whichever one you prefer. this also came in our chat cn add all cli command we're going to give this skeleton a key actually does not need a key my apologies we're going to give it a class name of rounded large px3 pa1 fight full text small width of 100 pixels and font semi bold and inside we are simply going to add a Unicode for a white space. So it populates the font, but so it puts the skeleton in the right, you know, height and width and everything, but it will not actually show anything. So it's still going to appear as a skeleton. And if we are loading, we also don't want to show this default all batch. So only if we are not loading, are we going to show this. All right, like this, there we go. And let's also now go inside of category section and let's add is loading value here. And this is how our skeleton is looking, right? Pretty good, right? But don't worry, we are just adding it here now. We will add it to the suspense. But before we do that, let's actually go ahead inside. And how about, yeah, I mean, I think that we should enable, we should make the value required as well. No, no, my apologies. The value does not need to be, does not need to exist always. But on select should probably always exist. What's the point of the carousel if you can't select an item, right? The value can be reset if we click on the first element here, which is the all. So let's do that. On click, on select, null, like this. So we are basically resetting the entire thing. Otherwise, if you click on this proper one, in that case, what we're going to do is we're going to call on select question mark because it can be actually no, it has to exist, right? So we can just execute it item dot value. Let me just collapse this items here. Some of you have asked me why don't I just use pre-tier for this. I use pre-tier when I develop the project, but I don't use it when I record the project because Prettier can sometimes change the stuff that's not recorded. Like I run a formatter now and it changes some code here. And then you're wondering why our code looks different. So that's why I don't like using Prettier while I'm recording a tutorial. But yeah, feel free to use Prettier. I do use Prettier. I do use formaters and autosave and all of those things, but not when I'm recording a tutorial. All right, so just make sure you have added on the real carousal iteration. On click, select item value. And also, yeah, no need for this because on select will always exist. Great. So now on select is missing. So how about we add on select here, X on the log X like this. Let's try it out. When I click here, there we go. You can see each of my elements. And in order to wrap up this chapter, we have to actually assign this category to our URL, to the search params so that it can be properly passed back here and show that it is selected. Before we do that, let's actually add a proper suspense here. So what we are going to do is we are just going to use the filter carousal here. And we're going to give it is loading prop like this. Oh, so it's missing on select. Okay. Yeah, let's give it data, an empty array, and on select, an empty arrow function. Not perfect, I know, but I think it gets the job done. And now we have a much, much smoother loading experience here, right? So you can see how this was my decision, basically. Whenever I'm using suspense, suspense query, just a component above it, I'm going to handle the suspense and the error boundary so I can decide how they are going to look like, and Not so that I have to go, you know, back to my view here and then see, oh, okay, so I have to change the suspense. I have to change the error boundary to some other component. No, I'm just going to have it all controlled inside of here. One step further, for example, could be categories skeleton. Simply to be explicit, right? I mean, I know this is funny now. This doesn't make any sense. But, you know, if you want to be explicit, have categories skeleton, you can do it like this. It's just passing the categories skeleton. It's the exact same thing. Don't export it. No need for it to be exported. But yeah, I mean, it gives you a bit of a larger control if in the future you want to add some more structure around the loading element. I think this looks very good. Let's wrap it up by actually adding the onSelect option here. So I'm going to add const onSelect value string null like this. I'm going to parse the current URL using window location href. If we have the value, I will do url search params.set category ID to the value. Otherwise, I will delete them from the search params. Make sure you don't add any typos for the category ID here. And then I'm just going to go ahead and add my router. from next navigation and I will do router.push URL like this and let's see URL to string. And let's go ahead and pass in the on select here. So now what should happen when I select here, you can see that my URL has changed to category ID and included the URL that I have clicked on. Now, there is a way to make this even faster because router.push is not exactly the fastest way of doing things because it will not prefetch anything. So if you want to, you can use router.prefetch as well. But perhaps this is something I will explore in maybe some other chapter. I will explore if maybe this filter carousal can wrap the batch element inside of a link component, because we can add the query elements, the search params to the link component as well. The link component will be much faster because it will prefetch by default. That's something to see. Maybe I will come back and do that. But at that point, we're almost certainly going to have to remove the filter carousal from being a reusable component and use it exclusively for categories. So I'm going to confirm with my project, with my completed project, if that's the case, then yes, I think we have room to make that optimization to make this even faster. Right now, you probably have no much opinion if this is fast or not. But keep in mind that later on, every time you switch this, it's going to query the database for a specific category and their videos. So then you might start to feel this being a little bit slower, even with our prefetching and everything. I'm going to see. Yeah, basically, this should all work for you. You should be able to change this URL and it should reflect the carousal here. In case it's not, here's how you should debug this. You should start from source page right here where you have the category ID. Make sure you didn't accidentally misspell the category ID. Make sure you didn't misspell it when passing to the home view. Make sure you did not misspell it here. and make sure you did not misspell anywhere here. So those are the only ways it can happen, right? Great. So I think that makes us properly prefetching the categories and we created the categories component. We can end this chapter. Great, great job. In this chapter, our goal is to create a studio layout. We're going to start by creating a studio route group, then the actual layout, and finally, we're going to protect the studio routes. This will be quite similar to our initial basic layout, if you remember it from chapter two. So yeah, this is what we're going to implement, and luckily for us, we can copy a lot of things from there. So just to confirm, this is the layout we are going to be building, right? So just this part right here. And the other part is going to be the page where we'll come to this later when we actually have some content that we can render now. But for now, we're just going to use this. As you can see, we will have this special component right here, which is something different from the basic layout. And this one will be maybe interesting because we are going to have to implement users procedure, which is something that we did not have up until now. And it will also be an example of us using the trpc use query without any prefetching or suspense. And I will explain why. Well, I can already tell you the prefetching doesn't really work inside of layout files So it best to use it inside of page files and just not do it inside of layout files And it will also be our first proper protected procedure You going to see all about that We can even try and prefetch so we can see if we get an error All right let go ahead and do that. Let's start by creating the studio route group. So we're going to go ahead inside of source app folder, and we're going to create inside of parentheses studio route group. Inside of this route group, we're also going to have an actual route named Studio. And now I'm just going to add a page.tsx inside. And I will simply return Studio back. And since we added this route inside of its own route group, it will not have this layout. So if you go to localhost 3000 slash studio, you will just get a text which says studio without any of the layout that we have because it's not a part of the home route group. Before we go on, let's go back to our localhost 3000. And how about we add a nice studio button here in this user dropdown. This is something that I briefly mentioned, I believe, but didn't really develop completely. So let's go inside of source modules out the components out button. And there we go. We have add menu items for studio and user profile. What we can do here from clerk next JS. Actually, I don't think we have to import anything. I think we can just open up the user button instead of having it be a self-closing component. And inside we can do user button dot menu items. And then we can do user button dot link. And we can simply, this is actually a self-closing tag like this. And we can give the userbutton.link a label of my profile, an href of, my apologies, not my profile. This will exist, but later. An href of slash studio and a label icon of clapperboard icon from lucid react. and let's give the clapperboard icon a class name of size 4 like this. So this is supposed to be menu items like this. So now if I click here, there we go. You can see that I have a studio redirect. In case you forget to put use client at the top here, I think that this will lead to errors where it says that userbutton.link is an invalid export. So in case you got that, it's because of this. You need to add use client to the top and then you will be able to use links here. If for whatever reason you are struggling to do this, well, don't worry. You can simply add a button outside of here, add a clapperboard icon like this studio and just add you know a link from next link to lead to studio so i will just give you an example just in case you're not able to add any menu items here you know there we go it's a button and give it an as child prop there we go and if you click here you can now go to the studio and it will only appear to logged in users so whichever one you prefer you can also give it a variant of secondary to make it look a little bit better. There we go. So a nice studio button here. If you prefer that, you can do that. Otherwise, you can just put it inside of the user button menu menu items. And also you can rearrange these things. So if you want to put manage account to the bottom, you can do that, but then you have to explicitly say that. So you have to add user button dot action and target it by label, which is manage account. Make sure you don't add a typo here. And then this will recognize that this comes from here. And there we go. Now manage account is below the studio, just in case you want to change that here. I will remove this to do here and I will add one here to do add user profile menu button. There we go. So now we can use our clerk user button to go to the studio. And now let's go ahead and let's develop the actual UI. So let's prepare by going inside of our modules and let's create our studio module here. Let's go ahead and create UI and let's go ahead and create layouts. And what we can do is we can copy, we can actually copy the entire home layout here. So how about we add it here, home layout. And what I'm going to do is I'm going to rename it to studio layout like this. We can ignore these two errors for now. Let's just go ahead and rename all instances of HomeLayout to StudioLayout here. Like this. There we go. So now let's go ahead and let's copy... Well, I think we can copy all components from UI here and just put it here instead of UI. And I think that now if I reload this window, everything should be okay. If I go inside of components, I'm not sure, do they depend on something here? No, looks like they're pretty self-isolated. No errors in each of the components. Great. So we just easily copied the entire layout and then we're just modified to work for our studio case. So let's go ahead inside of our app folder studio and we're going to create layout.tsx for the route group, not for the route, but for the route group, right? And we can copy the content from here because they're going to be pretty similar. Instead of importing the home layout, we're going to import from modules studio, studio layout, which in turn will be studio layout. Like this. So now if you refresh, you can see that I am on studio and everything is exactly the same, except it's not supposed to be the same. So let's go ahead and start by how would we first modify the navbar? I think that's a bit simpler to modify. So let's go inside of the studio layout and let's go ahead and let's rename the home navbar here to be studio navbar. And if you're being asked to update the imports, you can press yes. And that will update this import to studio navbar. So for now, we just change the name of the folder. So let's go inside of this new studio navbar instead of index.tsx. and let's replace this to be Studio Navbar as well. Now we have an error inside of our Layouts Studio Layout. We have to replace the name of Home Navbar to Studio Navbar. There we go. So now this is a Studio Navbar, but nothing's really changed. Let's go inside of this Studio Navbar here and let's modify it. So this image logo can stay, but this will change to Studio. So now when we are on Studio, this here says Studio, like that. And you can decide what you want to happen once you click on that. I think it's actually better if it stays on Studio. So when we are in the home, right, if I go to Cars and Vehicles and click here, I want it to reset to its homepage. But if I go to Studio and if I click on Studio, I want to stay on the Studio because we will have some sub-routes in the Studio as well. So I think it makes sense to change this as well. And we are actually not going to have to have any search bars, so we can remove that. But we do now have a little issue, and that is that we need spacer here. So how about we just add a div with a class name, flex1, simply for this to be pushed at the end. There are, of course, many ways you can do this, but this is one of them. Okay, so we got rid of that part, which we don't need. And now let's go ahead and focus on this. You can see that the studio has a border and it seems to have some shadow here. The same will be true for the sidebar. So that decision came from the original YouTube and YouTube studio. They have, for some reason, different layouts. So I will follow that as well. And I think it's nice because it's pretty clear that you're in a different application, or at least, you know, you're supposed to feel like being in a different application. So I agree with that. While we are here, let's remove the search input import, and we can remove it from here. Make sure you don't accidentally remove the one from your home, right? You need it here, right? Make sure you don't accidentally delete this one. We are working inside of a studio. All right. So yeah, technically, this no longer has to be a folder, but let's leave it like this. for now. And let's go ahead and modify this a bit by adding at the end of this Studio Navbar, border bottom and shadow medium. Like this. There we go. We can already see the difference clear as day. Now let's go ahead and let's close the Studio Navbar for a moment. And let's replace, I mean, rename our home sidebar to be Studio Sidebar and click yes to update the imports. and you can go back inside of your studio layout and you will see that now it has changed this. But this is still called home sidebar. So how about we change that by going inside of studio sidebar here and renaming this to studio sidebar. And now we have an error here because we have to replace both instances of home sidebar with studio sidebar. Nothing much to change yet, but you should get, well, different, nothing, nothing should change. Sorry. All right. Let's go inside of the studio sidebar here. And how about we start by giving some specific, I want to basically give it this borders here. I'm just not sure what is the correct place to do that. I think we can just remove border none here. And there we go. Now it has a nice border, as you can see right here. Great. And now we're going to have to modify the content here. And the content will actually be much, much simpler than in my home sidebar. So I'm thinking, do we even need this kind of separation here? I think it's easier if we just, you know, yeah. But okay, let's go ahead and still call this main section. But let's go ahead and, all right, let's see, how can we do that? Instead of having personal section, we're just going to do this. We're going to add a sidebar menu item from components UI sidebar. So make sure you add this input. Then we going to have sidebar menu button from components UI sidebar This will be a tooltip exit studio and it will have an as child proc Then we're going to add a link from next link here. And the href will lead back to the root page. And inside of here, we're going to add a logout icon from Lucid React. So make sure you have added this. Let's give the logout icon a class name of size 5. And let's give this a span of exit studio. And a class name of text small, like this. There we go. So now we have exit studio, but something seems up here. I might have forgotten something. I think that I need to wrap my sidebar menu item inside. Yes, it needs to be inside of sidebar menu in the first place, I think. Yes, yes, yes. Let's do it like this. Let's make sure to import the sidebar menu. There we go. And let's also add sidebar menu group. Is it called group or is it just sidebar group? It is. And I think that sidebar group should actually encapsulate the sidebar menu. There we go. So I think that now everything should be aligned nicely. There we go. So now we have the exit studio here. And what we're going to do is very simply, we're going to copy. We're going to copy. Let's see. I'm just trying to think of the best way to do this. All right. We're just going to copy the sidebar menu item here. So make sure you have two of those. And this one, very simply, is just going to lead to slash studio slash videos like this. And this will be an icon video icon from Lucid React. And this will simply say content. Actually, it can go to slash studio, right? basically this page right here. And we can use path name here. Use path name. And we can mark this one as active. So let's see. It should be the sidebar menu button is active. if path name is equal to slash studio like this. And we also need to mark this as use client in that case. There we go. So now it will say content here because we are on the studio. Let's go ahead and let's remove the main section entirely. Let's remove the separator. And we can now fix the indentation here. Remove main section and personal section. and you can remove the separator for now. Because this entire thing is much simpler, no need for us to separate that inside of its own components as we had to with the home sidebar, which clearly has different sections. But inside of here, we would just have content and exit studio, which should just lead you back to the root page. And if you click on studio here, you should go back to the studio. Great. So that seems to be working just fine. Let's see what I want to do now. So I do want the sidebar, but I want it to be the separator, but I want it to be here in between sidebar menu items. So just add back the side, the components UI separator. There we go. So we now have that. How about we go ahead and now create the header, which will load the user. Let's go ahead and let's prepare that component. So we are going to put it inside of our sidebar menu here. So it's going to be called Studio Sidebar Header, and it will accept no props. Let's go inside of the Studio Sidebar, and we are going to have to remove these two. But for now, let's just create the studio sidebar header.tsx like this. And let's export const studio sidebar header. And return a div header. Now let's go back inside of the index and let's import the studio sidebar header from .studio sidebar header. And you should see the header text here. Now let's go ahead and let's remove the unnecessary main section and personal section from the studio sidebar. Again, make sure you're working within the studio module. Don't accidentally remove your home module. You can always confirm by going back to ensure that you have everything here. Great. Now inside of here, let's go ahead and continue developing the header. I think that in the beginning of this chapter, I told you that we're going to have to create the user's router. Well, that's actually not true. Well, we don't have to. We can, but we don't have to. We don't have to rely on the database record because we can use the entire clerk object here. And clerk will actually have the most up-to-date data, right? Because every data change that happens in clerk will have to get resynchronized to our database. So we can actually safely just use this user from use user from clerknext.js. That's it. So this is what we are going to do. We're going to go ahead and change this to the sidebar header from components UI sidebar. And we are going to give this a class name of flex items center, justify center, and padding bottom of four. And then we're going to add a link component from next link with an href of slash users slash current. and then we're going to have to add a component called user avatar and I think we have it user avatar let's see user avatar we don't have it is it called avatar maybe it is it's called components avatar okay so this is what we're going to do we're going to go ahead and create this reusable component. Instead of source components, we're going to create user-avatar.tsx like this. And we are going to import the avatar from .slashtuiavatar, or if you prefer, componentsuiavatar, because avatar exists, but I want to create a reusable component for the user avatar so we can have proper tooltips and alt image and everything else. Let's also import cn from liveutils. let's also import avatar image here. And let's also import CVA from class variants authority and type variant props. If you're wondering what this is, well, if you take a peek at components UI and then pick anyone, for example, button, this is what it is. It's basically a util which allows us to create different variants. And it's going to be useful for us so we can create different sizes. I think it's just more manageable to work that way than using a bunch of ternary operations. So for example, we can do const avatar variants, CVA. And in the first argument, you just give it some default classes, which I think we don't need. We don't need anything to be here by default because every class name is going to be different depending on the variant. So add variants and a variant size. By default, we're going to have a height 9 and width 9. Extra small is going to be height 4 and width 4. Small will be height 6 and width 6. LG will be height 10 and width 10. And then we're going to have extra large, which will be height 160 pixels and width 160 pixels. And then let's add a default variance, size, and choose the default version. And that's it. We now have a nice way to control all the variants for our avatar. Now let's create an interface. User avatar props extends variant props type of avatar variants. Pass in the avatar URL. It can also be image URL if that's what you prefer. Let's call it image URL. It will be a required string. A name will also be a string. Class name, which will be an optional string in case we want to modify something. And on click will be an optional event as well. Let's export const user avatar here. Like this. Let's assign user avatar props. let's get the image url name size class name and on click and you can see how we have access to size even though we did not add it here that's because we extended the variant props so it inferred all the possible size types great so we have that and in what we're going to return is our avatar component our avatar image which will accept source to be avatar sorry image url and an alt of name and a class name here will be cn avatar variants and we're going to pass in size and class name and an on click there we go reusable and modifiable user avatar great now that we have that we can use the user avatar so user avatar component here from our components user avatar like this. Let's go ahead and pass it some props. So the image URL will be user image URL like this. Then we're going to have name, which will be user full name or user in case it's null. And we are also going to have a class name here where we are going to add a very specific size. I know this kind of breaks what I just built there. I built all those variants and I ended up not even using a single one of them, but I prepared that for all the future use cases of user avatar, right? And we are adding some transition when we hover here, because this will have, it will not have an on click, it will have a link around it. Great, so now what we have to do is we have to do this. If there is no user, return now. We are going to add a proper skeleton in a moment, but for now, let it work.\nlike this and there we go we now have a nice little when you click here it leads to 404 because we didn't implement users current yet but as you can see it's already starting to look like what we imagined here so let's go ahead and go outside of this link here and add a div here with two paragraphs like this in the first paragraph we're going to say your profile and in the second paragraph we're going to say user.fullname, like this. Your profile and then my name here. Let's go ahead and give this a class name of flex, flex column, item center and margin top of two. Let's give the first paragraph a small text and font medium. And let's give the second paragraph a text extra small and text muted foreground. There we go. This is looking much better. Now let's go ahead and let's actually create a skeleton for this. So we're going to go ahead and return sidebar header component. Let's copy the class name here so we don't have to write it again. And inside, we're going to render a skeleton from components UI skeleton like this. It's going to be a self-closing tag. The first skeleton will represent our avatar. So let's give it the size of 112 pixels and let's make it rounded. Below that, let's go ahead and copy this div class name here. And we're going to pretend to have two paragraphs, which are going to be two skeletons here. The first one will have a class name of height 4 and the width of 80 pixels. And the second one will have a height 4 and the width of 100 pixels, just to have a little variance between. So now when you refresh, okay, it's looking good, but we are missing something. We are missing gap Y1 here. And let's also explicitly add gap Y1 here as well. There we go. This is looking much better. Great. So now let's go ahead and just, I would like to wrap this to make it a proper if clause. I don't like how it looks. One issue that we have, I believe, is that if I collapse this, it looks horrible, right? So let's just fix that as well. In order to fix that, we can leverage our use sidebar from components UI sidebar. You can only use this if you are inside of a sidebar. So you have to render something inside of the sidebar like we do right here. And then you can use this hook inside. So you can access the state. And then you can check if state is collapsed. In that case, you can return a different sidebar menu item here. We have to import this. So let me just close it here. But I also added an import for sidebar menu item. Let's also add an import for sidebar menu button. And now let's go ahead and continue developing here. So we're going to have a sidebar menu button with a tooltip, your channel, and as child. And let's actually call it your profile because I will not be using the channel name anywhere. This will be a link component to users current. And we're going to have a user avatar here again. With image URL. Whoops, this is a self-closing tag. Image URL will be user image URL. And name will be user full name or user in case full name doesn't exist. And size will be extra small. And then we're going to have a span here, your profile. And a class name text small like this. So we are basically simulating as if we had, you know, this collapsed state here. There we go. So we can now handle both of these cases. And we also have a nice little loading here. I kind of don't like this jump here. I like to obsess over those details. Let's see. Can I do something about that? What if I added a gap 1.5 here? Or maybe two? Whoa. There we go. This looks much better, if you ask me. No visible jump now. Great. So we just had this layout finished. I think the only thing that's missing here is the create button. So let's just go ahead and prepare it because it will be a little bit different. It will be called the studio upload model. So we can prepare it. Right. Let's do that. Let's go inside of components here. And outside of the navbar, we're going to create studio upload model dot TSX like this. And we will export const studio upload model. But we are not going to develop the model yet. We are just going to add the button here. So let's add a button from components UI button. And let's add a plus icon from Lucid React and create. Let's give this a variant of secondary like this. And I think that's it. Nothing else needed here, but obviously we are going to continue developing here. Let's mark this as use client. So now let's go ahead and go inside of our studio, studio navbar index here. And let's go here where we render our auth button. and let's render studio upload model like this, which we imported from .studio upload model. And we should now have the create button right here. There we go. I think that is exactly as it was here. Perfect. And I think that is it. Yes. In the next chapter, we're going to go ahead and actually develop the functionality. And before we can develop the functionality, we have to create the videos schema. We have to push those changes to our database. And then we can go ahead and create a new video when we hit the create button. And then we're going to have to connect to MUX, which will be our video service, which is absolutely amazing. You're going to love it, especially with all the things that we are going to be able to do. We are going to give you a real YouTube experience. Our videos will be processing for some time, because they need to transcript and transcribe and all of those things. We will have subtitles. We will even have a bunch of AI features. You're going to see it's going to be very, very cool. But yeah, you should now have a studio layout here, which looks fairly different than the one from our home. Great, great job. Let's just see. Did we do everything here? I'm just going to enable my pen here. We created a studio route group and layout, but we forgot to protect the studio route. So let's go ahead and do that before I wrap up. So instead of source, instead of middleware.ts, instead of aiming for slash protected, we're going to aim for slash studio. So now if I happen to sign out, let me just refresh. And if I manually go to slash studio now, I should get redirected right here, sign in. And if I actually sign in, I am redirected back to the studio because that's the last place I tried to access. There we go. We just finished that as well. Great, great job. In this chapter, we're going to focus on creating studio videos. By that, I mean we're going to create the basic video schema for our database. After that, we're going to push those changes to Neon Database. Then we're going to create studio procedures so we can finally add video record, video record, record, sorry, creation, right? So we're going to enable this little button, which we created right here, to actually create that record in our database. And then we're just going to JSON stringify all the studio videos that we can load inside. Let's start by going inside of our schema right here. So inside of source, inside of database, inside of schema.ts. And inside of here, what we have to create is the videos. So let's go ahead and make sure that we have users defined, we have categories defined, and now let's go ahead and let's export const videos. So videos, pg table. Let's copy the ID, which is going to be UUID. Let's give each video a title, which will be a required text, so not now. a description, which will be an optional text, then we're going to prepare, let's go ahead and how about we do this? Let's copy this to created at and updated at. So these are the ones which we almost always need. And now what we have to do alongside title and description is we basically have to connect each video to a certain category and a certain user. But the category can be optional while user is required. So let's handle the user first. This is how we're going to do that. We're going to add user ID and we're going to add UUID and we're going to call it user underscore ID. And then we're going to add a foreign key reference. So references users dot ID. We are basically targeting the schema above, right? Users.id. That's why we knew that this has to be a type of UUID. Let's also mark this as not null. So it's clear that this is required. And since this is a foreign key, we can also give it specific instructions on how to behave on certain actions. For example, on delete, we want this video to cascade. So in case user ID has been deleted for any reason whatsoever, all of their videos are going to be deleted as well. But we are not done with defining this relation. What we have to do now is we have to import relations from our drizzle ORM. So let go ahead and do that Import relations from drizzle ORM And then we have to go ahead and do the following Let go ahead and go below our videos And let's go ahead and let's create export const video relations. Let's use relations, pass in the videos inside. And then in the second argument, you can destructure one or many, depending if you need one-to-one, one-to-many or many, right? all of those combinations. And now we are going to type in the user to be 1, users, targeting the fields, videos.userID, and references, users.id, like this. And we don't need many, like this. Now, before we push this, you probably have some questions. If we added this, why do we need this? Right? Well, thankfully, Drizzle RM has very good documentation on this. So if you go to the documentation and go into relations here, they will explain to you what they are. So the sole purpose of Drizzle relations is to let you query your relational data in the most simple and concise way. Be that using their relational queries like this, or what we are going to do, we're going to use SQL-like query builders, like using joins. Right? Now, instead of here, they go over all of their one-to-one, basically all of these things which we just did, like one-to-one, all of those other things. But if you click on foreign keys here, you get the answer to what we just asked. You might have noticed that relations look similar to foreign keys. They even have a references property. So what's the difference? Well, foreign keys serve a similar purpose. defining relations between tables, they work on a different level compared to relations. Foreign keys are a database level constraint. They are checked on every insert, update, and delete operation and throw an error if a constraint is violated. On the other hand, relations are a higher level abstraction and they are used to define relations between tables on the application level only. They do not affect the database schema in any ways and they do not create foreign keys implicitly. Basically, what that means is that relations and foreign keys can be used together, but they are not dependent on each other. We can define relations without using foreign keys and vice versa. This allows Drizzle to be used with databases that do not support foreign keys. So I hope that kind of makes it clear and we can even try it out. So if I go ahead and comment this entire thing out. This will work just fine. And let's go ahead and do this. So I'm going to go ahead and do Bunix DrizzleKit push. So now I'm going to push my schema and I actually have, you know, you can see it says pulling schema from the database and changes applied. So if I go ahead and run my studio now, let's see what's going on here. So I'm going to refresh this entire thing. and I now have videos as you can see here. And you can see that I have user underscore ID right here. And I have a proper relation to the user here. You can see that this user is not exactly a field. It's representing the relation. And if I go inside of users, at the moment, we also have videos here, right? So now let's go ahead and enable this video relations like this. Let's enable it. And let's see what happens if I go ahead and do Banex DrizzleKit push. If I'm correct, this should not notice any changes. And it did not do, and it did not notice any changes, right? So this relations right here, they work on application level. They are used so that Drizzle, I'm not sure how to express myself, but yeah, the application level would be the correct way to express. It doesn't matter what database you use. This is so that Drizzle knows when you are querying, when you're using your relations queries. Let me just go ahead and go back to this one. If you're using relational queries and if you want to do with posts true, you would have to add this type of relation if I understood it correctly. Which means that if we use joins, I think that we actually don't even need to have this type of thing here. Now, there are two courses of actions we can take for this tutorial, right? So if you want to, you don't have to type these relations right here. Though you would have to type them if, for example, you're not using Postgres. If you're using, for example, PlanetScale, which doesn't have foreign keys, in that case, you would have to add these types of relations. The other reason would be if you're using relational queries. I believe that these queries cannot work if you don't have defined relations. But if you use your normal select with joins, I'm 99% sure that you don't need this. Why am I even saying this? Well, I'm saying in case you're watching this far into the future where relational queries version 2 API has gone out. I'm now going to pause the screen and show you the GitHub pull request discussing the breaking changes which will come in the future in regards of that API. Here we go. This is the pull request that they have ready at the time of making this video. So it's been opened last week. it seems, and these are the breaking changes that are going to happen, right? You will still be able to access the relational queries version one by using database.underscoreQuery. For example, what does that mean for you? If for whatever reason you want to experiment with these types of relational queries and you want to use this exact syntax, you will have to add a little underscore here to access the version that I have in this tutorial. But that really doesn't matter to you because we will not be using relational queries. We're going to be using query builders. But I just want to bring to your attention that there are some breaking changes coming and I want you to be aware of them, both because it's a good practice to keep track of dependencies you're using, especially your ORM. That's a pretty big dependency for your project. And also, if you're watching into the future, I want to help you in the best way I can for you to move along with the tutorial. So you can go ahead and try and Google this. and you might actually find the pull request from drizzle.org. And you can see some other breaking changes. So the following imports are moved from drizzle.org and ORM slash relations to drizzle.org underscore relations, right? So those are some of the ways you can access the relations from version one in version two when this comes out. But at the time I'm making this tutorial, there is still some things that they have to complete. So in our case, we are going to write this just so we learn how to write them. But if you encounter any issues with this, if you notice, hey, this is throwing me errors, this doesn't exist, remember, you are not going to need them for this tutorial. So I'm purely doing this in order to teach you how to write these types of relations, which work on application level in Drizzle. And I think that you will be able to import this from slash relations in the future, I think. But for now, if everything works, if there are no errors, no need to change anything at all. One thing we're also missing here is to properly establish user relations here as well. So the foreign key has already done its job. It detected the user ID in the videos. And if you remember in our Drizzle Studio, when we clicked on the users table, we already had videos. But we have to add that manually if we plan on using the relations application level API. So let's do that here. So const user relations will be relations, users, many, and passing videos, many, videos. Just like that. And let's do export const. Again, if it's throwing any errors, you don't need to write this. Everything will work without it. And you can, again, try it out. If you try and push something, it will just tell you there are absolutely no changes in your database. You didn't add anything new here, which is correct, because what this does is just works on the application level. Great. So we have established a relation with videos and with users in two ways. One, using database constraint foreign key, which works because we use PostgreSQL. But in case you opt for using PlanetScale, for example, which doesn't have foreign keys or any other database solution which doesn't use foreign keys, this will be the proper solution for you. All right. So we have that. And now let's go ahead and let's define an optional category ID. So category ID will be UUID category underscore ID. And we will add references here as well. Category, categories dot ID like that. So the difference between this one, of course, is that category ID is not going to be required. And if we want to add some specific action here, we can do this. In case the category gets deleted, what we can do is just set null, right? We don't have to delete the video if their category gets removed, right? That doesn't make sense. That's a bit excessive. We can just set the category to null because it's not required in the first place. Whereas in here, I don't think it does offer us null as well, but that would, I'm pretty sure, break the app. So Cascade is the only option we can do here. Great. So we have that. Now let's go ahead and also populate it here. So we can just go ahead and copy this and just call it Category 1, Categories, Videos, Category ID, and inside of here, Categories ID, just like that. And let's go ahead and just copy the user relations here. And let's add category relations here. And I'm pretty sure it's the same thing. Basically, categories can have many videos like that. Great So we have this Now what I want to do in the videos is well push this schema because we did add a relation with the category. So let's do Bunex, drizzle, git, push. So this now will cause some changes, as you can see. But again, not because of this, not because of this, because of our category ID foreign key. So it's important that you understand that I'm probably being very repetitive now, but in case you're getting any errors with building with relations API, you don't have to do it. Just remove it from your project. Nothing's going to happen, right? As long as your foreign keys are working and you're using the same database as I am, I'm pretty sure you're not going to have any problems at all. Great. And, you know, just to wrap up my babbling about RQB2, the docs will come out by the time this comes out. So if you're really, really interested, you will probably be able to find it in the relations and you will probably see that this version 1.0 is much closer than 75% or is fully out, which means that the version 2 of the Drizzle relations is probably out and you will be able to read all about how they're being used inside of here. Okay, enough about me and relations. I hope that I kind of made it clearer. Now let's go ahead and once we've pushed everything inside of our database, let's go inside of our modules. Let's go inside of studio and let's create a server and inside procedures dot TS like this. Let's export const studio router and let's use create trpc router from trpc init like this. And let's go ahead and let's do get many and let's make that a protected procedure because only authorized users will be able to load their videos here. So let's go ahead and do this. I'm just going to go ahead and do .query here. Later, we will add something else here, but for now, I just want to demonstrate something. So what I'm going to do now is the following. I'm going to go ahead and get my data using await database from add slash database. I will select everything from my videos schema. So I have imported that. And I don't think I even need to add. Yeah, I don't really care about the fact that I didn't query by the user ID for now, because that's not what I'm trying to show you. I'm trying to show you one cool little feature, if we can call it like that. So a very simple procedure here. Now we have to add this to our router here. So studio will be studio router like this. I like to order them by length. If you think that's completely insane, feel free to not do that. Okay. So now what we're going to do is we're going to go inside of our source app folder studio. Studio again, page.tsx. We're going to turn this into an asynchronous method. We're going to do void trpc from server.studio.getmoney prefetch infinite. So I had no problems getting prefetch infinite, but I will have a problem calling use suspense infinite query, and I'll show you why in a second. So let's just add our hydrate client here from the server as well. And let's just add studio view here. like this. Now let's go ahead inside of our modules, inside of the studio, we already have a UI folder. So now let's open up a view here. Let's go ahead and let's add a studio view.tsx studio view. And this is not a default export. This is just a UI component. inside of here, what I'm going to do is just open up a div and we can add a videos section here like this. And let's go ahead and create our sections here. And let's go ahead and add our videos section dot DSX, which is finally going to be a use client with videos section component here. videos section. And now inside of the studio view, we can import the videos section component, and we can go back inside of our app studio, studio page, and we can import the studio view, which is hydrated, and it will have the cached and prefetched data. And if I now go inside of my modules studio UI sections, the video section right here. Let me just refresh this to confirm that the videos section is being loaded here. If I now go here, for example, and if I try to do the following, I will try to get the data from trpc from the client. I will choose my studio and I will choose get many. Right now, oh, it looks like it works. Use suspense infinite query. Okay, I was trying to demonstrate something, but I failed. What I was trying to demonstrate is that if you name your query a certain name, I believe it will not always give you a strictly typed use suspense infinite query. I don't know if you remember, but this did happen to me in my very first procedure that I've created. So this was a big fail on my side. I wanted to demonstrate something that is not true. So let me just try and make that happen. I'm going to go inside of my trpc. You don't have to do this. I'm just trying to show you something. Let me bring back my hello base procedure here. You don't have to do this. So just a random query here. And let me return. I've already forgot how to even write procedures. So I will just write this. Return hello world. Like this. I think this is a normal base procedure. I think that if I try and change this to rpc.hello. All right. no matter what I do, I cannot reproduce it. But in case you're having any problems with use suspense infinite query not appearing for you, there is a fix for that. One fix would be just reloading your window. But the second fix is what we are going to do. All right. So I'm going to stop with all the explanations and demos and I'm going to remove this hello base procedure. and I will focus by going back inside of the studio router. Basically, what's important for you to trigger that you suspend infinite query in case it doesn't work for you is to add a proper input here. And inside of this input, you have to import Zod. So let me just go ahead and import. Do we have Zod? Let me just go ahead and see. So Z from Zod. We do have Zod. So basically what we need in the input is an object. This object needs to have a cursor. And this cursor can then be whatever you want. And once you add a cursor like this, you will almost certainly be able to get studio, get many, and then you will be able to do use suspense infinite query, right? I'm not talking about this error. This is a different type of error. In my case, it was not even allowing me to autocomplete use suspend infinite query because it noticed that it's not something that can be prefetched in that way. All right. So one important thing that is very logical, but still I've managed to mess it up. So I will give you a tip for that. You have to be careful. If you're using prefetch infinite, you have to use suspense infinite query. You cannot do use suspense query here and mix it with prefetch infinite or vice versa. Use just prefetch here and then mix it with use suspense infinite query. This will cause errors. So you have to be certain about what you want. In 99% of the cases, if we do get many, that's why we're going to be concise with our naming. If we do get many, it means that we're going to do infinite querying, right? We're going to do infinite loads. So go ahead and add prefetch infinite here and also prepare uSuspense infinite query here. Now let's go back inside of our studio router procedures here and let's properly define this cursor that we will expect. It will accept an ID, which is going to be a string and a type of UUID. So that's how we are going to know what's the last item we are left off. And we are also going to need to have updated ads. So basically another key to have this infinite loading cursor work on. And this entire cursor can be nullish, meaning that it's not required, especially if this is our first request, right? The cursor will only be used as a paging method when we need to know from where to start querying. And besides the cursor, we're also going to have a limit, which will be a number with a minimum of one and a maximum of 100. You can, of course, define this differently if you prefer it. And now this is an important thing. So inside of prefetch infinite here, I'm going to add a limit 5 like this. This also means that inside of my videos section, I have to do the same thing. I have to do limit 5 here as well. And where is one more thing missing here? every time you do use suspense infinite query you need two arguments first one will be your basic input and the second one is get next page param which you basically are telling react query okay how do i get the cursor well that's going to be last page next cursor this will throw an error right now because we don't really return anything back from that query so it's confused right now So we are going to fix this in a moment. So let's go back inside of this procedure now. But before we do it, there is one more thing. I just, I think I didn't finish my thought. If you add limit five here, you also have to add limit five here. So I think a good way to do this would to just establish some global constants So instead of your source how about we add some let see inside of my source I will add a new file Why can I name this I just want to add a new file and name it constants.ps like this. So inside of my, in the root of my source folder, I now have constants and I will export const default limit and I will make it five. So now that's what I'm going to use everywhere. Default limit from add constants. like this. And I will do the same thing in the videos section, default limit from constants. There we go. Now let's go back inside of our studio procedures and let's go ahead and add a proper get many query here. So the first thing I want to do is since this is a protected procedure, that means that we can go ahead and extract from here context as well as the input. So from the input, we can destructure the following. We can destructure the cursor and we can destructure the limit. But from context, we can get the user from the database and we can get the ID. But I like to remap it to user ID. You can do it like that or you can do it like this. But I don't like having user here simply because sometimes I will fetch the user and I will name the constant user here. And I will rarely need the entire object here. So I'm okay with doing context.user and then id user id. Or you can do like deep destructuring, I believe. So user and then this. I think that ends up being the same thing. For some reason, I like this more. So I'm going to use it like that. ID, user ID. This is also called an alias. I'm basically renaming this because ID is a bit too generic. I want to know that this is the user ID. All right. So now let's do a proper query here. So we are going to query from videos. But what we have to do is we have to add a where query. and equals videos.userID needs to be our user ID. So we no longer have to use the clerk user ID here because this is an actual database user. It has a clerk ID, but we don't need it in this case. Let's go ahead and import equals from drizzle.oram right here. And now what we have to do is we have to implement a proper pagination here. So in order to do that, we have to improve our where query. So let's add the following from drizzle.oram, and or larger than, and I think that's going to be it. Let's also add descending. Yes, and or larger than and descending. All of these things are what we are going to need in order to do vagination here. So let's first wrap our very simple equals query, which will simply load all videos from one user. Let's wrap that inside of and like this. and then we can add some combinations here. So if we have the cursor, that means we have to create the following query. If we have the cursor, let me just do or here, and then just already prepare undefined here in the end. And now in the or, this is what we're going to do. We are either going to query by larger than videos updated at and our cursor updated at, or we are going to query by and both videos updated at and cursor updated at and larger than videos ID and cursor ID like this. Let me just go ahead and indent these two. I think, yeah, there we go, like that. And this can be collapsed. I just want it to be indented properly. Okay, I think this is fine. Great, so we have that. And now what we have to do is we have to add .orderBy here, descending videos updated at and descending videos ID like that. And we also have to add a limit, limit plus one. This limit comes from the input. So whatever we wrote, how many records to load, but we need to add plus one. Why are we adding plus one? add one to the limit to check if there is more data. So we are always going to check for one more item than whatever the frontend requested simply so we can calculate if we actually have something else to load in the next batch. So let's go ahead and do const has more to be if data.length, data being what we just loaded here. So if data.length is larger than the limit, In that case, we have more. Now, what we're going to do is the following. Remove the last item if there is more data. So const items has more. In that case, data slice 0 minus 1. Otherwise, just use the data. So if we do have more, we have to remove this extra item, which we used to check to see if there's more. Right? We don't want to bring that back to the user. The user is not supposed to know that we even loaded that. We are only using it for ourselves to know, okay, there is more data after what user just requested. Great. So we have the items. Now let's set the next cursor to the last item if there is more data. So now we have to set the cursor to whatever is the real last item, not this plus one, the real last item in this data which we loaded so that the next iteration knows where to start loading from. So const last item is items, items.length minus one, a very simple array usage here. And then we can define the next cursor. So if we have more, in that case, next cursor is going to have an id of last item.id and updated at of last item updated at. Otherwise, it's going to be null, meaning that the cursor can be completely undefined. There we go. Now that we have that, instead of returning the data, we can go ahead and we can return both the items and the next cursor like this. And now once you've done that, if you go inside of your Studio UI sections video section here, you no longer have an error here because we are returning the next page cursor. So this is the information that we are going to use to query for the next page. Great. So we just did that. And if you go ahead and try now and do JSON stringify here and pass in the data, you will simply get nothing here, right? You can see nothing in the page params, nothing in the next cursor, nothing in the items, but you can see the structure which we have here, right? So this is what we have to do now. We have to go back inside of the procedures here. And besides get many, we also need to create our create procedure. But here's the caveat. I don't want to put that in the studio router. I want the studio router to only be responsible for loading videos on the studio page. I want that to be a protected procedure so I don't have to dynamically pass in the user ID. I just want this route to only be accessible if you're in the studio. So what I'm going to do instead is I'm going to go ahead inside of my modules and I will create a new module called videos. And inside server and then procedures dot TS like this. So I'm just going to go ahead and copy this basic import here. Like this. I will remove the, okay, actually I do need the protected procedure. I just think I don't need any of these here. So what I'm going to add here is first of all, rename this to video router. Is it, or maybe it's called videos router. Yes, it's called videos router. So let's go ahead and add create here, which will be again, a protected procedure. So we only want the author to create this. So this will be a mutation. Let's get asynchronous context here. And we can destructure the ID from context user because we are logged in at this point. And basically, what we're going to do here is create the video. So await database insert into videos and pass in the values. And we're going to pass in the user ID. The title will be undefined, untitled actually. And I think that's all we need right now. And in order for this to be populated here, you need to add dot returning. Otherwise, it's not going to return anything back. So you can just go ahead and return back video, video like this. There we go. So that's a very simple procedure, which we're going to use to create new videos. So now let's go ahead and do the following. Let's go inside of source, inside of trpc routers here. Let's add videos, videos router like this. and now how about we go inside of our source app folder my apologies modules we have to go inside the studio navbar right components studio navbar actually studio upload model this is the place we need and let's go ahead and add create to be trpc from the client dot videos dot create\ndot use mutation. So just added this and then on click here, how about we just do create dot mutate like this. So what's going to happen now? If I go ahead and click create here, nothing should really change. But if you refresh, you should now see data here. If you're not seeing data, go ahead and check inside of Drizzle Studio. I'm not running Drizzle Studio, so let me just do Bonnix Drizzle Kit Studio here. There we go. So I should now have one video here. In case you cannot see anything here, you better check for logs here. You can see that I got post 200 okay for creating my videos. You might have gotten an error in your case. Otherwise, if you can see something in the database in the videos section, perhaps you did not connect it to the user properly, right? You can see that I have, my user now has videos. If I click here, I can see the untitled video here for my user. So if all of that is, if you can confirm in your database that all of that is true, but you still can't see anything here, then it must mean that you did something incorrectly inside of your studio procedures here, inside of the get many method. So just double check that you've added equals videos user ID to this user ID from the context user like this. Great. So we have that, but it's not exactly perfect, isn't it? Because if I click create here, nothing happens. I have to refresh in order to see the next item. This was usually problematic in server components because this is where we are technically fetching these videos instead of studio page.tsx. This is where we are fetching them, right? So it was always problematic. Okay, do I now do router.refresh, right? Do I do revalidate path? What do I do to make this fetch again? Well, here's the cool thing about our procedure. We don't care about this part anymore. We are only using it as the initial loader to prefetch something. After that, everything ends up being on this client right here. This client from now on takes care of any type of revalidation here, which means that if you go inside of your studio upload model here and you add utils from trpc.utils, use utils here, open up on success here, and you call utils.studio.getmany.invalidate. Can you already guess what will happen now? I'm going to refresh my entire thing here. I will click create, and there we go. You can now see the changes happening in real time, leveraging both the prefetch from the server components for a very fast initial load and also leveraging the fact that the rest is done on the client, meaning you are familiar with that, right? This is not new to you. So we literally get the best of both worlds. I hope that it's getting clearer and clearer how we're going to use this prefetching thing, right? I think it's easier to understand it when you actually have some real world examples. So that brings us back to this point, right? Remember when I explained this to you, we use this server component to prefetch the data. But once we prefetch it, all this auto refresh, I probably should have called this revalidation, not auto refresh. So all this revalidation only happens in this layer here. It doesn't go back to the server component. You don't need any router refresh or revalidate or revalidate path, or I don't even know what exists anymore, right? It's all very familiar. Great. So I want to do a couple of more things before I wrap up. So the first thing I want to do is inside of my studio upload model is just disable this button. If create is pending like this. And I also want to do this. If create is pending. I'm going to use one icon called loader to icon from Lucid React. So just to have, oh, I'm also missing, this is disabled. And this is supposed to have a class name of animate spin. So let's try it now. There we go. This looks much, much smoother now. Perfect. And we can also do another thing here. So if I go inside of source, inside of app layout, inside of my TRPC provider, I can add a toaster. from components. Actually, I'm never sure where I have to import this from. Okay. From components, UI Sonor, that is the place where you want to import this from components, UI Sonor, don't accidentally import it from toaster. So make sure you just add it somewhere near the children like this. Then go back to your studio, upload the model. And what you can do here is you can do toast.success and you can just say video created. And you can import toast directly from Sonar like this. So now let me just refresh first. There we go. Video created. And you can also handle the error in the same way. And what's cool about this TRPC thing is that you can actually be very confident about your errors here, right? So if you want, you can do toast.error, error.message, right? And then if you were to go inside of your procedures, instead of studio server here, my apologies, video procedures, where you create yours, and let me throw an error on purpose. So throw trpc error, which I've imported from trpc server here. Let's see what happens if I just Passing the code, you know, bad request. TRPC error is not callable. I'll throw new TRPC error, bad request. So let's see what happens when I refresh this. If I click create, there we go. It just says bad request right here. You can also specify a message if you want, you know, specific message. So I think that now if I click create, it will give me a specific message, right? So you can go ahead and do that as well. I'm only interested in what happens if, you know, an uncontrolled error happens. What if I just throw a new error here? Oops. Will that also work? Let's see. Okay. So it manages to do these things pretty well. So I guess that we can rely on it, right? The reason I'm doubting this is because usually whenever, you know, I work with errors, they are never this reliable. Now, they are always in some weird objects. And I can often get, you know, some weird either JSON stringifications here or the app breaks. But I guess if the types say that this is a string that will always exist, I think we are in the clear. But if you prefer it, you can also just do a generic something went wrong here and not use the error at all. Depends on how explicit you want to be with your users. Do you want to tell them exactly what went wrong? Do you think that's information that should be obfuscated for the user? It depends on the type of app you're building, right? I'm going to stay true to the source code and I'm going to be using this. But keep in mind that you can use the error and then just get error.message inside of here. Great. So I'm going to stop it here. There is one more thing we have to implement, which we're going to do in the next chapter. And that is the infinite load. Because we have prepared our data for infinite querying, but we did not do it on the UI side. And while I have you here, let's just learn this. what happens if I go inside of studio page. And for example, if I forget to prefetch, let's see what happens then. So I'm going to refresh this and everything seems to be fine. Yes, it's a bit hard to reproduce this issue, but I think I am getting the issue right here. And this is the issue, unauthorized, right? So let me try and refresh more and more. You can see that I keep getting the unauthorized. I will try and add some space here to confirm. Am I still getting it? Yes. You can see that I am basically getting TRPC client error unauthorized, right? Why is this happening? Well, it's happening because it lost the headers somewhere in between the server component and finally inside of my videos section here. So you have to be careful about this. Whenever you are doing prefetching, whenever you're doing useSuspenseQuery, you need to have the equivalent prefetch. This brings us back, you know, when I was explaining to you that the React query team has something in place called strict query or query strict wrapper, which will warn you, you know, on the project level, like, hey, you forgot a prefetch or you have an unnecessary prefetch, things like that. But, you know, you can always just do use query if for any reason you don't want to prefetch, right? So if you don't want to prefetch, you can just use query. But in that case, you know, this changes the data, you know, basically things get different then. We are going to cover the normal examples as well. You don't have to worry. I'm going to show you how to do all kinds of things. And also, if you prefetch, you also have to change this to use suspense query because I'm not exactly sure what kinds of errors will appear. Let me see. I think that maybe the exact same error will appear. If I refresh here. Yeah, it's if you miss the wrong combination of prefetch, it's almost as if you didn't write the prefetch at all. So if you prefetch infinite, you need to use suspense infinite. All right, I'm going to stop here and we're going to do the rest in the next chapter. Amazing. So let's just go inside of here and let's use our handy tool. We created the video schema, we pushed the database changes, created studio procedures, and we added a video record. Amazing, amazing job. In this chapter, we're going to go ahead and wrap up what we started in the previous chapter. One thing that's missing is infinite loading. So let's do a short chapter on how to do that We already have the backend part ready Now it time to implement the UI part Besides that we also going to add suspense and error boundaries to our videos section component And we're going to create a reusable infinite scroll component. And we're also going to demonstrate how it works. So let's go ahead and do that first. Let's go inside of our videos section. And what I like to do here is rename this to videos section suspense. and then export comes the videos section like this. And then what I can do here is I can return the suspense from React and error boundary from React error boundary and then finally videos section suspense. Let's give this a fallback of error and let's give this a fallback of loading like this. Nothing much should change, but when you do a hard refresh here, you should now see loading for a few seconds. Now let's go ahead and let's actually implement the infinite scroll component. In order to do that, we first have to implement a reusable hook called useIntersectionObserver. So useIntersectionObserver.ts. Let's import useEffect, useRef, and useState, all from React. Let's export const useIntersectionObserver, and let's go ahead and define the options to be IntersectionObserverInit. So this is a globally available type. Inside of here, we're going to prepare our state to tell us whether we are intersecting or not. So is intersecting, set is intersecting. Basically, we will use the observer to detect if the user has reached the bottom of a list so that we can safely initiate a function call. Let's also define a target ref to be use ref. and let's add html div element here. Let's call the use effect. Let's define the observer here to be new intersection observer. Execute it and then inside of its method here go ahead and get the entry. Return the arrow function here and simply do set is intersecting to the entry is intersecting like that and passing the options here. The options will be our optional prop coming from here. If target ref dot current, if we have the target set, right? So we're going to add an empty div at the end of our list. For example, videos section. Imagine that at the end of this list, we have a little div with the ref target ref, right? And then we have our use intersection observer hook here. So basically, the intersector will look if we have reached this with our UI. So for example, on this screen, it will immediately call the next request because it will be immediately rendered here. But if I was, you know, zoomed in or on mobile, I would have to scroll and then we will see that little target ref. So you can remove it for now. This was just a demonstration. So if we have that target ref, we are just simply going to tell the observer to observe the target ref dot current here. And of course, we have to have a proper cleanup method here to disconnect the observer. So we don't cause any overload here like this. and pass in the options as the dependencies here. And you can return back the target rough and is intersecting. Like this. There we go. Now that we have the use intersection observer, let's go ahead inside of our components. And inside of here, we're going to create a reusable component called infinite scroll.tsx. First of all, these are the props we are going to have. So interface infinite scroll props. Optional is manual boolean, has next page boolean, is fetching next page boolean, and fetch next page method. Now let's export const infinite scroll. And let's simply assign the props here. like this. Let's go ahead and destructure all the props and set is manual to be false by default. Inside of here, what we are going to render is a div with a class name of flex, flex column, items center, gap four, and padding four. And then we're going to render our very simple ref, which will be the target ref. And we will give it the class name height of one. So now we have to add our use observer. What did we call it? Use intersection observer. So our use intersection observer hook from hooks use intersection observer. And from here we can get the target ref and then we can pass that here. There we go. So this is what we are going to try and detect if we've reached it or not. So instead of here, what we can do is we can pass in the threshold if we want to modify it, for example, 0.5 and root margin to be 100 pixels. This is the configuration that I found to be working quite well with my infinite load needs. And now let's add a use effect method here in which we are going to have to decide whether to call fetch next page or not. So if we happen to be intersecting, which we can return from here is intersecting like this. So if we are intersecting and if we have next page and if we are not already fetching the next page And if we are not set to is manual, only then are we going to decide to fetch next page. And we also have to add all of these items here. So is intersecting has next page, is fetching next page, is manual. And fetch next page, all of them. Right, so make sure you didn't forget any exclamation points or accidentally added some exclamation points here. Great, so we have that now. And now what I'm going to do is the following. I'm going to check if as next page, in that case, we will render a button component from UI button. Like this. Let me just change this to components UI button. Inside of this button, we're going to check if is fetching next page. We're going to render loading. Otherwise, load more. Variant will be secondary. Disabled will be if it has no next page or if it's fetching the next page. And on click here will manually call fetch next page. So this will be kind of like a fallback. In case the intersector for whatever reason doesn't work, the user will always be able to manually click load more, right? But that's only if our backend returns that we have next page. Otherwise, we will return a paragraph, which will say you have reached the end of the list. Class name, text extra small, and text muted foreground. Like this. And now we can go inside of the videos section and we can add the infinite scroll component. Now this infinite scroll component, of course, needs its props. So let's go ahead and do the following. Besides data, we're going to get the query. And then inside of here, we can pass in hasNextPage to be query hasNextPage. is fetching next page. The query is fetching next page. And fetch next page will be query fetch next page. And for now, what you can do is you can also add is manual. Just set it to true. This will automatically fall back to true. So now you will be able to see the button load more. And when you click load more, it will load more until it reaches the end of the list. The load more will, of course, depend. If you don't have it, it means you probably don't have more than five items in your database. So you just go ahead and spam the create a few times, of course, until you have more than five items. Let's see what the error is here. Okay, if this happens to you, remember we did set up our, where is it, in my protected procedure. I have the rate limit here. So if you want to, you can increase this to 100 requests in 10 seconds and then try again. There we go. So you should be able to now test out your infinite load here. and what we're going to do now is the following. Let's go back inside of our video section and this time let's remove is manual. Let's see what's going to happen. So I'm going to refresh the entire thing and you can see how it automatically starts loading. So that's how our infinite load is going to work. If the observer detects this, it's automatically going to call it in the use effect, but if for whatever reason it fails, unless it has loaded every single data possible, which are back and will ensure that it does, the user is always going to have that button. But if for whatever reason we want to turn off the observer, we just set is manual to true, and then it will work like this by the user clicking on this. Great. So we are finished with that. And I actually want to leave it at this I trying to think of the best course of action for our next chapter at the moment I think that the next chapter should be starting to implement mux, actually connecting to mux so that we can have some proper videos, some proper thumbnails, and all of those useful things so that we can actually render this data here in a nicer way. but there are some things that we can already do here. Let's go ahead and let's go back inside of our studio UI and let's go inside of a view studio view right here and how about we give this div a class name of flex flex column gap y of 6 padding top of 2.5 then inside of here add a div with a class name, key X of four, and an H1 element of channel content, like this. Below that, a paragraph, which will say manage your channel content and videos. Let's go ahead and give the H1 element a class name of text to Excel and font bold. And let's give the paragraph here a more of a secondary class like text extra small and text muted foreground like this. So now you should have channel content written right here. If you ever get this type of error, you can just reload your window. This seems to be the TypeScript server going wild. Let's go inside of the videos section and let's actually create a nicer display of this data here rather than just a stringifying. it. In order to do that, we're going to have to import the necessary components from our table. So ensure that you have add components UI table, as I do source components. Can I close everything else? Close others. My apologies, not this one. Close others. Like that. There we go. So inside of my components UI, you should have the table component here, and you should be able to add all of these imports. And now what we're going to do is the following. We are going to go ahead and give this div here another div with a class name border on top and bottom. And then inside, we're going to render a table, table header, a table row here, and then a table head. This table head will simply say video first here with a class name PL6 and width of 510 pixels. After that, the next one will be visibility, which is not going to have any special classes. After visibility, we're going to have status. Then we're going to have a date. then we're going to have views, comments, and likes. And for these last three, we are going to have a class name, text write. And the last one will also have PR of six. So right now you should have this table here, video, visibility, status, date, views, comments, and likes. Now let's go ahead and remove the JSON stringify here. And while we are inside of the table, outside of the table header, add a table body here. And inside of here, we're going to go over our videos. So let's go ahead and rename this from data to videos. And what we're going to do is we're going to combine all the pages available. So videos.pages.flatmap, get the page, page.items, and then .map and get the individual video. Like this. We're going to use the link component here and give this an href of slash studio slash videos and then video ID. And the key will be video ID as well. And then inside of here, we're going to add a table row. and this will have a class name of cursor pointer. And instead of a table row, we can just add a table cell here, which will render video.title for now. So just make sure you have added the import for link from next link like this. And we also have to, yeah, the infinite scroll can actually be outside of the table. It doesn't have to be inside. And you can now see that we have some items here. It's not exactly perfect. And there are some errors going on around here. So we are going to go ahead and just resolve this now. So let's see what exactly is happening here. So inside of this, for each video, we are rendering a table row. We're rendering a table cell. So how about we add some more table cells here? So we need a table cell for visibility, which right now doesn't exist, right? Then we need one for status, I believe. Then we need one for date. Then we need one for views. then we need one for comments and I believe the last one is for the likes. All right so now we can see untitled visibility status date views comments likes. All right and let's just see if I'm doing something incorrectly here. I think that I might be missing something. Let's see, because this does not look. Oh, yes. I think that our link needs to have legacy behavior on. There we go. Yeah. If you add legacy behavior, then you can use link to wrap your table row. Right. In case you cannot put legacy behavior on, there is a solution. Well, you're going to have to remove the link wrapper. So you're just going to have the table row. So go ahead and add a key here, which will be video.id. And then you're going to have to add the router from useRouter from nextNavigation. So you didn't have to do this. I'm just showing you in case you get stuck. You would have the router. And then in here on click, you do router.push studio slash videos and then video ID, which will eventually go to the same thing, which right now is 404. The router.push is slower than link because link will prefetch the route. So I'm going to roll this back without using useRouter, and I'm simply going to use the link wrapper with my legacy behavior prop, because without legacy behavior, it breaks the component. So I'm just going to add legacy behavior here. This is something that exists inside of my next 15.1.6. All right. Great. So we have that now. and I think that we are okay with ending the chapter here. I think we've wrapped up our infinite component and we prettified this look right here. What we have to do next is we have to actually connect to Mux, our video service. We have to find some videos to upload. And then based on that info, we're going to add some more fields to our schema for the videos right now, because the videos are pretty simple, only title and description, actually. But later on, we're going to go ahead and give this a thumbnail. We're going to give it a preview URL. We're going to give it a bunch of useful things, including some MUX specific states as well. So that's our next goal. Nevertheless, this is definitely starting to get some shape. Great, great job. In this chapter, we're going to go ahead and implement Max Video Service. We're going to start by creating a responsive dialogue. On desktop, this will be a normal model. But on mobile, this is going to turn into a slick drawer component like this. Let's go ahead and do that first. I'm going to go ahead and go inside of my components here. and I'm going to create a responsive dialog.tsx like this. I'm going to go ahead and import use is mobile from hooks use mobile and then I'm going to import everything I need from my dialog and from my drawer and you can see that the imports mostly match the dialog and the drawer and then the equivalent content header and title. Make sure you add all of these imports. You should not get any underline here because both of these components should exist in your project. Great. Now let's go ahead and let's create an interface responsive model props. And let's rename this to responsive model actually. There we go. Responsive model. We're going to give it a children of react, react node. We're going to give it open of boolean title of string on open change. which will accept open and return avoid. Let's export const responsive model. Let's assign these props right here. And let's go ahead and destructure all of them. Children open title and unopened change. Now let's use our isMobile hook. Use isMobile. This hook was automatically generated with Shatsy and UI in case you were wondering where does this come from. Now let go ahead and do if isMobile return and let create a drawer We going to give it an open on open on open change on open change and inside drawer content drawer header and drawer title and render title inside Outside of the header, we will render the actual body, which will be the children. Otherwise, we're going to return a dialog. We can copy the props open and on open change here. and inside add a drawer content, drawer header, and drawer title, and render the title inside. Outside of the header, you can simply render the children again. That's it. That's our responsive model. Now that we have the responsive model, let's go ahead and try it out. Looks like I'm not using dialog content dialog header. Yes, my apologies. Instead of dialog, I'm supposed to use dialog here. There we go. Make sure you don't do the same mistake as I did. So in your normal return clause, it's dialog, dialog, dialog, dialog, dialog. Inside of isMobile, it's drawer, drawer, drawer, and drawer. Great. You should have no errors in this code. Now let's go ahead and let's go back inside of our modules, Studio, UI components, and let's go inside of Studio upload model. What we're going to do now is we're going to render this responsive model. So let's wrap this entire thing into a fragment. By default, nothing should change. If I go inside of my code here, this should still just render a button here. But what I'm going to do now is I'm going to go ahead and add a responsive model here from components responsive model like this. And inside of here, I'm simply going to add a paragraph saying this will be an uploader. And let's go ahead and give this a title, upload a video like this. And let's give it a value of open for now. And let's give it on open change for now to be an empty arrow function. And now, as you can see, if I just refresh, if you're on slash studio, basically wherever you render this create button, you should be seeing a dialogue. But if you switch to mobile mode, it will change to a sleek drawer like this. Great. So our responsive dialog works. Now let's go ahead and let's properly open this. So this is what we are going to do. We are going to leverage from this create method. Well, we can actually, yeah, we don't have to destructure it. What we can just do is the following. we are going to open this if we have the following create dot data like this. So if we get the data, in that case, we are going to open it. And on open change here can simply call create dot reset like this. So let's try it out. Let's see if this will work. So when I create something new, you can see that it will only open a model once my data exists. And then when I close this, it will simply reset this mutation, meaning that we are no longer going to have any created data from here. So this is kind of my solution to use derived state. We could, of course, have created an actual is open state and then change it depending on on success state. But, you know, why shouldn't we just use derived state from here, right? So this should work just fine. Great. And this is pretty much it as to how much we can do without creating a MUX account. So let's go ahead and learn a bit about MUX, my decision to use MUX, and so on. So we are now going to create a free MUX account, which means that credit card is not required. The reason I emphasize this is because I know that some of you are students, some of your credit cards are not accepted depending on where you are in the world. So I tried to find a service which allows this kind of access to their product, which is enough for us to complete the tutorial. And I have some experience with video services. I have a platform of my own and I actually switched through two video services. I first used Vimeo and then I decided that it's not good enough and it eventually became quite expensive, almost $1,000 a year for not that high of a traffic. And then I switched to Bunny CDN, which is great. It's a great tool and it's quite cheap, but it's not even close to MUX, I would say. It's a great tool, but I am considering switching to MUX now, especially with all the new features that keep coming to MUX much faster than they are in any other video service. Besides that, Mux is used globally with extremely, extremely high prestige clients. From NBA to Shopify to all of those big companies that you can find on their website, they truly are the video API of the internet, right? Nothing comes close to them. Chances are if you ever watched a live stream online, if you ever watched any kind of video platform, they are using some services of MUX. Not only that, it is also the most cost-efficient video service platform, right? So it's not just some random video service that I found. I truly try to find the best solution for a YouTube clone. And besides that, it will give us everything we need. And the good news about this all is that they don't have a credit card requirement in order to try out their product. They don't even have like a limit in a sense of locking your account. No, it's free forever. But there are some limitations in the free Max account. So these limitations are the length limit of the video. You will only be able to upload videos, which I think are around 30 seconds. You can try and upload a longer video and I think they will just cut the video at 30 seconds. The second limitation is that the video will be deleted 24 hours since it's been uploaded. and you will also have a MUX watermark. So these are the limitations. None of this will really cause any problems for our tutorial, but keep this in mind in case you want to go into production. So what happens if you want to go into production? What can you expect in regards of pricing? Well, I will tell you this. I added a credit card for my initial development of this project and I spent exactly $0. And I unlocked the full benefits of MUX, meaning I can upload up to 12 hours. Videos are, of course, never deleted by themselves, and there are no watermarks at all. And I spent $0. I believe that they have a very, very high threshold until they actually start charging you, which I think will require around 100,000 views for a video before they start charging you. You can, of course, visit this, calculator.mux.com. and I calculated for my platform, codewithantonia.com, and I think it will come up around $10 a month, which is extremely cheap considering that I do have a certain level of traffic and over 170 hours of content. So it's quite cheap considering all that. But in case you don't have a credit card, you can still continue with the tutorial. Don't worry. Besides Max's account, you will have to either create or obtain a very short video with English audio. I would highly recommend finding something with English audio or simply recording a 15 second clip yourself. If you, for whatever reason, can't do that, you can visit this tinyurl.com slash newtube slash dash clip. I will, of course, I will open this in a moment and you will see, and then you will find a short demo.mp4 video, which lasts 15 seconds and it's just me speaking into a microphone in English. The reason I emphasize on English audio is because we are going to demonstrate subtitle generation for English videos. And then later on, I'm going to use the transcription for some AI features. So that's why I'm emphasizing this. And 50 seconds is only because that will be a very small video. So you will be able to test faster. You don't have to wait for upload of a megabyte sized video. And also, remember, length limitation, right? Great. So we can actually mark our responsive dialogue as finished. Now let's go ahead and let's create a Mux account. So head to mux.com and go ahead and click on Login. Once you're inside of here, you should be able to see this screen. Store and stream video files online. create your first asset and all of those things. And you will also see this right here, upgrade to remove all restrictions. And the restrictions are the watermark, the duration limits, and the fact that videos are deleted after 24 hours. Don't forget about this in case you're using this, you know, for your school project or want to demonstrate this to someone and 24 hour passes and your videos get deleted. Don't be surprised. This is why it happens. If you want to upgrade, You can go ahead and click upgrade. I chose this option, which is pay as you go, which means that they will not automatically start charging you anything until you actually start reaching something. As you can see in your plan, it will be included 100,000 views per month, right? So I think that it's very hard for you to accidentally reach 100,000 monthly views. So I think you will stay within the free tier. This is if you want to upgrade. You don't have to. For example, I will not upgrade for this tutorial here. Great. So what I want to do now is I basically want to create this, this uploader right here. And we can do that by first creating an environment. So I'm going to go ahead and create my new environment here. And I'm going to call this new tube like this. And I will select development. So let's call this new tube development like this. And I'm going to go ahead and select all users and click add environment. And I should now have new tube development right here. And I should be able to click on host and stream video here like this. And now how about we go through the docs together and learn how to add the necessary packages and environment variables for MUX. So let's go ahead and start with integrate with your app right here. And I'm going to select new to deployment development and the permissions here. Let's see, let's select Max video. Well, let's select all of them, right? I think it's okay for us to have them all. The access token name will be new to.\ndevelopment and let's click generate token. And now you have the access token ID and the secret key. And as you can see, they don't store the secret key. So memorize it or simply download it. So let's go ahead and let's add these keys to our project. So I'm going to go ahead and go inside of my project instead of dot environment dot local. And I'm going to add max token ID. That's going to be the shorter key here and then max token secret. Right. And this will be exclusively for this environment, which we created new tube development, right? So make sure that you are inside of here. Every time you visit this page, you know, just make sure that it didn't automatically maybe select development or production here. I decided to use this one, our new one, so that it's very clear that this is for that project in case you want to reuse this account of yours for any other projects you might build. So I created new tube development here and I clicked integrate with your app. I selected new tube development here as well. I selected all of this and then I clicked generate token. I believe that you can also find the token in the settings here. There we go, access tokens. There we go, new tube development. But I can only see, as you can see, the MUX token ID. I cannot see the MUX token secret. In case you think yours got lost, you can just click generate a new token. Again, select new tube development. And I'm not sure if we need all of these, but it's not going to hurt us to have it. Okay. Great. Once we have that, let's go ahead and let's look at the documentation here together. And specifically, I want to focus on uploading the videos. And we're going to use the MUX uploader for web. Basically, this is what we are going to build. So let's go ahead and see. I'm not sure if we need this because I think there are instructions specifically for Next.js. I just want to see. Okay, this is for playing the video. I just want to confirm that. There we go. MaxUploaderReact. I think this is what we're going to need. So let's go ahead and install that. But, you know, be careful. Just wait for you, wait for you to see, wait for me to add it so you can see the version that I have. So I'm just going to add it to my project and then you're going to see the version. So just a second. Which one is it? There we go. So 1.1.1. So what you can do to ensure that you have the same version is this, bun add max max uploader 1.1.1. Let me just confirm in the package JSON, max uploader react 1.1.1. Great. So we now have that. So let's go ahead and develop our studio uploader here. So I'm going to go ahead inside of my modules, inside of my studio, instead of UI components, and I will create a studio uploader.tsx like this. And let's go ahead and create an interface studio uploader props, which will accept an endpoint and on success here like this. And let's also import everything we need from the max uploader, which we just installed. So from max, max uploader, we need a default export max uploader. And then we are going to have this kind of granular max uploader drop file, select progress and loader status. This will allow us to customize it to make it a little bit prettier because you can see on their documentation. Let me see on the max uploader here. It's not exactly the most beautiful uploader. It's okay. But we're going to make it look a little bit better like this, more similar to one on YouTube. All right. So now what we have to do here is we have to actually develop it. So let's export const studio uploader. Let's go ahead and destructure the props here. Endpoint on success. And now inside of here, let's go ahead and give this a div and a render max uploader here. And I think by default, we should already kind of see this. Let's go ahead and do, instead of Studio Upload Model, just render the Studio Uploader. You can ignore the TypeScript errors for now. So if I go ahead and refresh this and click Create, there we go. I can already see an uploader here, right? the one thing that's currently missing, as you can see right here, is we are missing, let me just find, okay, so the React usage was here, okay, we are missing an endpoint, right, so endpoint is a value that we need to pass to our max uploader, and in our case, we're going to have to create that endpoint, and then we're going to have to create some webhooks, because remember, videos can take hours, or if not days, the process, like for example, my videos in YouTube, which last 12 hours, they definitely take a couple of hours to process. And we can't really await that in a promise. We need to set up webhooks in order to make that to work. But let's first solve this. Pass the URL to the endpoint property on the mux uploader component. So whenever we upload a video, we're going to have to reach a specific endpoint. In order to obtain the endpoint, we're going to have to install the node instance of mux. So again, I'm going to do mux from mux-node. You can wait a second if you want to see the exact version. So this is my exact version. So for you, you can do one add mux slash max node at 9.0.1. You should have mux node. And then what you can do is you can go inside of your source instead of lib and simply create mux.ts, import mux from mux, mux node. and export const mux to be new mux. And inside of here, you will pass the token ID and you will pass the token secret. And we have both of those inside of our environment.local, mux token ID and mux token secret. So let's add them like this and mux token secret like this. You can even point exclamation points at the end. So then you won't have any errors here, but looks like there are no errors with or without this exclamation points. So that works fine. So now what we have to do is we have to go back inside of our video module, specifically our procedure where we created the video. And what we have to do is we have to create an upload, right? So this is what we're going to do. Const upload will be await mux, which you can now import from lib mux like this. mux.video.uploads.create. And then we're going to open up new asset settings here. And we're going to add a pass-through of user ID. The pass-through is a field, as you can see, arbitrary user-supplied metadata that will be included in the asset details and related webhooks. can be used to store your own ID for a video along the asset. So basically what a pass-through is, is, well, let me remind you, when we upload a video, it's not going to be immediately processed. We're going to have to wait for a webhook. And the webhooks are anonymous. Webhooks will not come from our logged in user. So we need a way to preserve which user has uploaded this video. Otherwise, Max will simply fire a webhook on our part, and we're going to have no idea whose video is this. That's why we need some metadata and we can store that through pass-through. And we're going to use the database user ID for that. Great. So we have the pass-through. Set the playback policy to be public. MP4 support will be standard. So these are the settings that I found in their documentation, right? Nothing other than that. And I believe we also need outside of here, of course, origin. and we're going to set it to all like this, just an asterisk. But I'm going to give a little to do here. In production, set to your URL. But just so we don't mess up, you know, the development, you know, course can be quite annoying. Of course, it is secure, but it can also be quite annoying. So this is how we create an upload, right? And once we create an upload inside, we will be able to pass the following. we will be able to pass URL upload dot URL like this. So now what's going to happen is that if you go inside of your studio upload model, every time that you create something, you will have alongside your video also a URL, right? And then you will be able to do the following. You will be able to pass an endpoint, which will be your create dot data dot URL. Now, of course, this is what we're going to do. So I'm going to check if we have create data URL. I think I need to add this. Otherwise, we're going to render a loader to icon. And let's just close this. So let me collapse this so it looks a bit nicer. Like this. There we go. And we can be more specific here as well. So let's specifically check if we have data URL like this. Great. And here we can also pass on success just to be an empty arrow function for now like this. All right. So now we should properly establish the endpoint to where this should upload. And now it basically knows since we're using our muxlib to create this URL endpoint, it knows that it has to upload it to this specific environment, right? So these tokens come from, my apologies, from this environment right here, new tube development, right? Always, whenever you click on assets, always change to new tube development, right? So those are the keys for this environment. That's why this upload URL is important. And I think we can already tried out So here what you can do If you didn obtain a video either go ahead and create it or simply go to tinyurl slash YouTube dash clip Let me go ahead and copy this So I going to go ahead and go here and it should simply open this public Google Drive which I've created. And you can just go ahead and download a simple demo and before video. It looks like I'm getting some error here, perhaps something wrong with my settings. I'm going to see if that's something I have to change here. But basically, it's just a regular video where I'm speaking in English subtitles here. And let's go ahead and try on uploading it. So I'm going to refresh everything here. I will click create here. Oh, it looks like something went wrong. So let's see what that is. Looks like I got 500 back. So let's see how we should debug that. I'm going to go inside of my procedures here. Looks like we have an upload here, but maybe this fails. All right, so I think I've debugged our issue. If I click create here, I get an error here. And in the network tab, I can then see an error here, which says invalid parameters, deprecated standard MP4 support is not allowed on basic assets. So that's probably because when I developed this initially, I used an account with my credit card added. So I think I have to remove this. Let's see if this will now change anything. If I click create, there we go. Video created. and we actually have the proper upload created as well. So now I'm going to go ahead and upload this demo video. So let's go ahead and go inside of your assets here. Select the new tube development. Make sure you're here. And if you upload it, you should see it there. So I'm going to select my demo video here. Oh, it looks like no URL or endpoint specified. Cannot handle up. It looks like it's not exactly working as intended. So I'm going to go ahead and do console log. Upload. And below that, I'm going to do a console log. URL, upload URL. Let's go ahead and debug this together. See exactly what's going on. I'm going to add some space here. And let's click create. So it looks like it. I can definitely see the URL here. That's interesting. Oh, I think I know what's the issue. The issue is not in our backend. The issue is that after I pass the endpoint in the studio uploader, I never use the endpoint. So let's pass in the endpoint. Like this. And let me try again now. Demo.mp4. There we go. Now it's uploading and it just says upload complete. Nothing more, nothing less. If I go here, I can finally see one new asset right here. and this is what it looks like. So it is a free test asset. It looks like it's limited to 10 seconds, right? And it will be deleted after 24 hours. So I'm just going to turn off the audio here and you can see it is for API test purposes only, which is exactly what we're doing. We're testing the API to see if it works, but you can see the status. It's ready. You can see that we have the duration of the video and all of this other useful information. I'm going to see if we can, if it will allow us to generate thumbnails on the basic tier. I hope it will, but it's okay. We will find some other solution if it doesn't allow us to do that in free tier. Great. So now let's go ahead and, well, discuss how should we exactly, you know, track that this video has now been uploaded. But if you take a look in my BonaX Result Kit Studio right now, and if I go here to Studio Result, there is not a single video that I can tell you, okay, I know which MUX asset is for this video, right? We have no idea neither which user uploaded this nor for what video did it uploaded because our video database entity and MUX asset are two different entities. So we now have to find a way to connect them together. And we're going to do that the following way before. So I just want to do that before we actually spend some time with the studio uploader. Let's go ahead and do the following. I will close everything and let's go inside of our database schema here. We're going to go inside of the videos and we're going to add everything we need for regarding MUX. So after description, go ahead and add MUX status. And yes, I will prefix them with MUX. Simply, you know, for us to have it easier understanding, okay, this comes from MUX, not something else. So MUX status, MUX asset ID. This will be unique for each of our video. We're going to have MUX upload ID again. This will also be unique. We're then going to have MUX playback ID, MUX upload ID, my apologies, playback ID, which will also be unique. MUX track ID, unique as well. And we're going to have MUX track status, MUX track status, like that. So these are all the elements we're going to need for MUX. So the MUX status will basically tell the user what's the state of their video. Are we still transcribing the video? Are we uploading it? What's the state, right? Are there any errors or something like that? The asset ID will be something we will receive once the asset was created. The upload ID is something that we are going to immediately populate when we create the video inside of our videos procedure. So because we have the upload here. So we are simply going to pass the upload ID inside of here. And then when the webhook comes, we're just going to look for that upload ID. And then we're going to connect the asset with the video. The playback ID is something that will come once the asset has been created. And we're going to use the playback to play the video and to generate thumbnails or previews. the track ID and the track status will only be available if the asset has subtitles. So if we can do any kind of transcription, in that case, we will see the track ID and the track status as well. Great. So let's go ahead and do that now. So we're going to go back inside of these procedures. And this time what we're going to do is we're going to set the MUX status manually as waiting. as in we are waiting for a video to be uploaded in this place. And we are going to set the max upload ID here to be upload.id. Now, of course, this will not work just yet because we have to push this schema changes. So let me shut this down and do Bunex drizzle kit push. I believe you even might get some errors at the moment right now because you can see max status does not exist. So feel free to shut down the app and simply do Bunex run dev all here. Now, I got a prompt here, what I want to do with this, simply because we have 20 items in our videos table. In our case, it really doesn't matter what happens. You can select both yes and no. I'm going to select no just to see if it will work. Let's see no again, no again, no again. I'm going to select no for all those fields. And it just says changes applied. It really doesn't matter for us because all of these videos are just, you know, empty videos. Matter of fact, how about we delete all videos so we have a clear example. So let's do Bionics Digital Kit Studio. Let's open the studio and I will just remove all of my videos because all of them are empty anyway. There we go. Great. So I'm just going to refresh my app here. and we're still not totally ready to connect our asset with the video because what we're missing is the webhook. So that's the next thing that we have to do. So let's start by going inside of our mux. Where is it? Here. How about we go into settings and let's click on webhooks right here. Now again, make sure you change this to new tube development and click create new webhook. And now we have the URL to notify. And luckily for us, we already know what to put here because we used webhooks before. That you can find in your dev webhook here. I hope you did this because it's quite useful. You can see that now with one command, we are connecting to all these webhooks that we will use throughout our project. So this is HTTPS. Make sure to put HTTPS in front like this. And I'm going to click create webhook here. And now I can click on show signing secret and copy this. Let's go inside of our environment here and let's add max webhook secret and add it here like this. There we go. Now let's go ahead inside of our source app folder API and let's go ahead and let's create videos. And then let's go ahead and let's create a webhook and let's create route.ts like this. and this will basically be our videos webhook. So let's export const post request to be an asynchronous request. Like this. This is our webhook. Obviously, this will translate to localhost 3000 slash API slash videos slash webhook. So I just remembered we also have to modify that here, I believe. And I think I've made a mistake by creating this like that. how about we remove this one delete the web hook and create a new one uh and just make sure you select new tube development here and let's go inside of my package json so i can obtain the url again so https this slash api slash web slash videos slash web hook so make sure this is properly written, right? Let me just confirm. Slash API slash videos slash web hook. API videos web hook. Great. And then you will have to copy the new signing secret and go back inside of your dot environment dot local and replace it right here. There we go. Great. So now this works. And now What we have to do is we have to first of all prepare our signing secret to be process and then just copy Don type it out because you can add some typos here without you knowing. Great. Besides this, what we're going to have to do is we're going to have to prepare the equals from drizzle.org so we can find by user ID, by upload ID, something like that. And we're also going to need from mux, muxnode slash resources slash webhooks.mjs. Looks like it also works if you just go slash webhooks. And from here, we're going to import all the types we are going to look at. So video asset created webhook event, video asset error webhook event, video asset ready webhook event, and video asset track ready webhook event. Great. And then let's go ahead and let's create one unified type called webhook event. And this should use all of these from above. So our webhook event can be one of these types. All right. Now, inside of here, first things first, we're going to check if we are missing the signing secret. And in that case, we can just throw new error back. max webhook secret is not set. And let's just say true, max webhook secret. Great, okay. Then what we have to do is we also have to import headers. Let's see, headers from next headers. We have to do that. So let's obtain the headers payload. To be await headers. Let's get the MUX signature to be headers payload, get MUX signature. If we are missing MUX signature, it means that whatever is trying to access this API endpoint is not MUX. It is something most likely malicious here. So no signature found with a status of 401. If we do have the signature, we can then go ahead and get the payload from await request.json. I'm not sure if we defined request. We did not. So the request comes from here. It will be a type of request. This is a native type. All right. So we have our payload. And then let's also get our body, which will basically be a stringification of the payload. and then let's call mux from lib mux i will separate that import here to verify this headers and everything so mux.webhooks.verify signature pass in the stringified body select mux dash signature here to be mux signature which we get from the headers make sure you don't add any typos here because anything any anything wrong here can break the verification and pass in the signing secret. That's it. This will throw an error in case something goes wrong. Now let's do a switch here on payload.type as webhook event type. And in case we have video asset, my apologies, video asset created, in that case, what we're going to do is we're going to update the video record in our database with the appropriate upload that has happened. So this is what we're going to do. Const data will be payload.data as video asset created webhook event and simply target the data. If case we are missing data upload ID, it means we can't really find the relevant video. So, I mean, this should never happen, right? If this happens, something went wrong on MUX side. So let's just return new response here. No upload ID found. And we can return back a status 400 because this is not something we can work with. Otherwise, we can go ahead and update the video's schema. So make sure you imported the video's schema here. and we can set the mux asset ID to be data.id and we can set the mux status here to be data.status and we also have to import the database from add slash database. My apologies. So we are updating these two because we now have them and let's pass in the where to be equals videos mux upload ID to match data upload ID like this and break like this. Now let's just go ahead and outside of the switch here, let's do a return new response webhook received just so we don't throw... Webhooks always expect a success request at the end And if you have too many failures of a webhook, they might actually shut down your webhook. So always be careful to do that. So I think we've written this correctly. I think this should now work. Basically, what we're doing here is the following. When we go inside of our videos procedures, remember, we first create the max upload. And we store the upload ID inside of our newly created video entity here. So let's go ahead and demonstrate this now. So I think this should work already. I think everything should be fine, but you never know. So just make sure you do bond run dev all. So you should have your web hook running. So if I go ahead and copy this, and if I go here, I should see my app running. There we go. As you can see, I can see my app. Now close this and work in local host, right? But you should just confirm that that is working. So what I'm going to do now is the following. I'm going to click create here. And now video has been created. Don't close this just yet. If you now go inside of Drizzle Studio here and refresh, make sure you have your Drizzle Kit Studio running as I have Drizzle Kit Studio. And if you now go inside of videos, you should be able to see a max status of waiting because we are waiting for a video to be uploaded and we have the max upload ID. So once the webhook hits our server, we're going to use that upload ID from MUX and tell our database, hey, I have a video which I prepared this MUX upload ID for. Can you find that video and now give it some new information like MUX asset ID and MUX status, right? So let's go ahead and try that. So for now, mux asset ID is completely empty, as you can see, as well as, you know, mux track ID, mux track status. Everything is basically empty besides mux upload ID. So what we are expecting to happen is that mux status changes to ready and mux asset ID changes to an actual ID if we've set everything up correctly. So I'm going to go ahead and upload my demo video here. Let's wait for this to upload. this should just say upload complete and if I now let's see in webhooks max can I I'm not sure if I can now keep track of my events here I think that's one thing that I I'm missing from here it's not exactly clear you can exactly you know keep track of what's going on but what we can do is we can look at our terminal and looks like something was definitely hitting our AVI videos webhook and it returned 200, which means that our verification was correct, which in turn should mean that if I refresh this, there we go. It still says preparing. So it's still doing something in the video, but the max asset ID is right here. Perfect. So this is exactly what we wanted to achieve because now our video is correctly associated with a max asset, right? It's no longer random as to which video we uploaded and which video we have in our database. Great. So in case this is not working for you, start troubleshooting from your terminal. Are you seeing API videos webhook? If you are seeing them, but there is an error here, you are most likely doing something incorrectly in regards of your webhook verification. So just be careful and go ahead and read it slowly line by line. In case you want to see the documentation for that, you can click on listen for webhooks and you have verify webhook signatures here, which will guide you through the same thing that I was telling you about. And then they will basically give you a generic pseudocode on how to do this. But you can also click on node and they will tell you how you can verify the signature. But I think this is the only thing that can actually fail at this stage. So perhaps you are missing the webhook secret. Make sure you didn't misspell this. Make sure that you actually have it in your dot environment right here. Make sure that you didn't accidentally add the wrong one. But I think it will give you some useful information if an error happens. Great. And furthermore, if you think that the verification is not the issue, but you're still getting some errors, you can add some console logs here. So I'm just giving you some guide on how to debug things, how I would go ahead and debug this. Great. So this seems to be working. And I don't think there's any point in us going further with all of these other types. We will, of course, add them. But for now, this was the most basic one. OK, the asset has been created. How can I connect that asset to my video database? Great. We've done that. Now what I want to do is I want to wrap up the tutorial. My apologies, the chapter, not the tutorial, with making this uploader look good. So let's go ahead and do that. We're now going to focus on the uploader. So feel free to click create and just leave a video here. Let's go back inside of our studio uploader. And we should have all of these unused files right here. So this is what we're going to do. We're going to go ahead and give our max uploader here an ID because that's how the styles will recognize for what it is. So I'm going to call this video uploader like this. I'm going to give it a class name of hidden, and then I'm going to give it group slash uploader. Basically, I'm going to hide it. It's not going to be visible because we are now going to build our own. So we need to hide it CSS with CSS, but it still needs to exist. It still needs to be rendered here. That's why we need the ID of it. And now let's add our max uploader drop component. And let give it a max uploader of video uploader Let go ahead and do this How about we add this to a constant So const uploader ID ID, and just add it here, and then use the uploader ID in both of these places. There we go. And now let's go ahead and give this a class name of group slash drop, so we can differentiate between different groups and what happens inside. And then we're going to use this native web components using slots. So all of this comes from Mux documentation on how to customize the uploader. I think they have customized look and feel here. And then you can see that you can customize all of these things using subcomponents. So basically, that's what I researched. And now I'm just telling you, I'm showing you my finished result of how I managed to do it. Great. So slot heading will have a class name of flex, flex, column, item center, and gap of six. Inside of here, we're going to have a div with an upload icon from Lucid React. So just make sure you have added the upload icon here. I'm just going to move that here. And I think that already we should see the upload icon. And we also see the text, which says, or that comes by default in the heading slot, I believe. Let's go ahead and make this upload icon a bit better by giving its wrapping div a class name of flex, items center, justify center, gap of two, rounded of full, background of muted, height of 32, and width of 32. There we go. Already looking better. Now let's give the upload icon itself a class name of size 10. TextMutedForeground. Group. Drop. And then you're going to have to open square brackets. Add an end sign. And then open square brackets again. Active. And then animate bounce. So this will be a cool effect that when we like drag something. Let me try. okay i'm not sure how exactly to demonstrate it now but if you drag something it's going to start to bounce uh but i think that i missed something here let's see group uh drop active okay i'm gonna leave it i think it's okay for now let's add transition all and duration 300 like this we're gonna come back and fix it if it doesn't work outside of this div add a new div with a class name, flex, flex column, gap two, and text center. And inside of here, add a paragraph with a class name, text small, and it's going to say drag and drop video files to upload. And below that, another paragraph, your videos will be private until you publish them, because we will add privacy settings later for the videos. Let's add text extra small and text muted foreground here like this. And then let's add max uploader file select here. Give it a max uploader of uploader ID. And add a button from components UI button here. So make sure you have added this import. Let's close the button inside. And the button will simply say select files. The button will also be a type of button that's important and have a class name of rounded pool. So now you should also have, you know, this nice input or you can click select files directly here. Great. And now go ahead outside of this div and add a span with a slot of separator, which is basically this, the or text. And we're just going to give it a class name of hidden. It can even be a self-closing tag. And now the OR text should disappear. Below that, we're going to add our MUX uploader status. It's going to be a self-closing tag. It will take the MUX uploader status. It will take the MUX uploader to be our uploader ID. And class name will be text small. and below that we're going to have a mux uploader progress self-closing tag as well which will have a mux uploader of uploader id class name of text small and type will be percentage and max uploader progress again which will be max uploader uploader id and type will be bar so I want to display both of them because they look good together now let's go ahead and try it out now so I'm going to click create video created and let me go ahead and select this and there we go I think this looks much nicer we have both these uploads and at the end we have upload complete here. Great. So yeah, you might have noticed that there is a potential issue that we have is that if we click create, the video has been created. And then if we just close this, that's it. This video will forever stay without its equivalent record. So I was contemplating which solution do I have for this? And I ended up leaving it like this because we will have a status which will basically say waiting. So the user can always, you know, will be able in the future to go inside and just delete that record. We can also create a cron job or a background job, which will recurrently look for videos like that. But there is no, I couldn't find like a nice way to do it because we cannot exactly know when an asset has been connected to the video which has been uploaded, right? What we could do is we could redirect to slash max upload ID and then use that in a route to find the video. That's one solution maybe. Yeah, that's something we can explore later. This is the solution I went in the end with. But yeah, I will explore it once again later on. Just in case you notice like, hey, I can create as many of this and they will never be connected. Yes, that is an issue. But you're going to see how it looks like when we actually have some data. It will actually look like something's wrong with the video. It will show like a sad face because we couldn't load a thumbnail. So it will indicate to the user that they can delete this. But perhaps we can't think of a solution to automatically handle that or avoid that from happening in the first place. Great. But I think that we achieved everything we wanted in here. So let's go ahead and use this. We created a free account. Credit card not required. At least I managed to obtain a 15-second video. I hope you did too. And we created the upload model. So that's all we wanted from this chapter. We successfully integrated MUX. Great. So, and we also have this cool responsive dialogue. And you can check out how that looks. Yeah, we forgot to check that. When you click create, there we go. Looks very, very nice. Perfect. So we have all of that here. And what we're going to do next is we're going to explore some more events that we can track. Let's just see what's the error here. Oh, on success here. Let's add the on success to the max uploaders before we wrap up. Like this. So just add the on success here. And what we're going to do next is we're going to explore some more of these cases, which we can use to update our video assets. And then we will finally have some thumbnails and things like that to display here for more information. Great, great job. In this chapter, we're going to go ahead and continue using MuxWebHooks to populate some more information in our video databases records. Let's start by creating the UI part first so you can understand why we need the video asset ready event. I think that's going to make a bit more sense. So let's start by going inside of the public guest that I have prepared where I told you that you can find the logo for the application. Well, now I've prepared a little placeholder for you, which we're going to use when a video doesn't have a thumbnail set. There are two ways you can obtain this. You can, of course, use any image you want, or you can simply download one from my guest here. Or if you have access to the source code, just find the public folder and you will find a file called placeholder. So the way I'm going to put it in my project is I will literally copy the SVG. If you want to, you can just drag and drop the file. There we go. That's it. That's my file. So what we're going to do now is the following. We're going to go inside of source, inside of modules, videos, and we're going to create the components, my apologies, UI. And then inside, we're going to create components like this. And then finally, you can create the video thumbnail.tsx like this. Let's go ahead and export const video thumbnail here. Let's return a div with a class name of relative. Let's go ahead and add a comment here. This will be our thumbnail wrapper, I guess. So this will be that div. And then below here, I'm going to add a comment duration box, video duration box. right? We're going to have that here. You do add video duration box, just for you to understand why we're going to have this wrapper first and then a thumbnail wrapper again. So both of this will need to be relative. So let's go ahead and add relative here again. Pull width, overflow hidden, transition all like this. Actually, we might not need all of these. So let's just add overflow hidden here, rounded, extra large and aspect video. Later on, we're going to add some more classes here, but it will make more sense once we can actually, I'm kind of keeping information from you because I want to naturally build this. But since I already know how this is going to look like, basically, we're going to have two image elements inside. One will be a normal static image, and the other will be\none will be a GIF, right? An animation. So when we hover over this element, we're going to hide the static image and show the animation playing. So we can show a few snippets from the video to the user without actually loading the video. So that's why I'm telling you that we're going to need to have some additional classes here in the future. But for now, all I want to do is just render this placeholder, which I just put in my project. So now let's add an image from next image. Let's give it a source of slash placeholder.svg. an alt of video or a placeholder, actually a thumbnail. And let's give it a fill property like this. We're also going to give it some class names here. Let's give it size full, which, yeah, if you don't know, size dash full is the same thing as writing height full and width full. So whichever one you prefer. And let's also add object cover here like this. And I think this will be enough to try out the video thumbnail. So now what I want to do is I want to go inside of my studio UI sections, video section. And inside of the first table cell where we rendered the video title, we are now going to render that thumbnail. So add a div here with a class name, plexitemscentergap4. Another div here with a class name of relative, aspect video, width of 36 and shrink 0, and then a video thumbnail inside. Now the video thumbnail will be imported from ModulusVideos UI components video thumbnail. And if you try it out, you should now see the empty thumbnails here because none of our videos at the moment have thumbnails. So, of course, we are using just the normal slash placeholder SVG. So, just double check that inside of your public folder, you have the placeholder.svg here. Great. So what our job is to do now is to use the handle video asset ready webhook right here and actually populate the thumbnail. So let's go ahead back inside of our route for the video. Inside of source app folder API videos route. and we have video asset created. So what we're going to do next is we're going to do case video asset ready like this. Now inside of here, what we have to do is we have to find the asset, which we previously updated. So now we can use the asset ID if we want to. We can use either max upload ID because I think both will be preserved in the data. So first things first, when we handle a new event, we have to get the data. So let's copy the data from here, data, and let's use video asset ready webhook event. So you should have this imported as well. And now you have the new data here. And this is what we're going to do. First, let's get the playback ID. We can get the playback ID from data, playback IDs. And by the time we get to video asset ready, we should almost definitely have it like this. But as you can see, it's still throwing some errors. So we do have to use the optional chaining methods here like this. Actually, it looks like we don't need it here. We just need it here. So we have to put it like this. And then what we can do here is if we have no playback ID, we can immediately stop this, right? or we can just return a new response, missing playback ID, because without this, there's nothing much we can do for this video. So I'm just going to throw back an error here. Then what we are going to do now is the following. We can easily get the thumbnail URL by using the playback ID. So the thumbnail URL is made by going to image.mux.com, playback ID, and then thumbnail.jpg. That's how you create a thumbnail using Mux and the playback ID. And we have to wait until the video asset is ready. And only then can we obtain the playback ID because the playback ID does not exist here. As you can see, they have made it optional because there's a chance it doesn't exist. But I'm pretty confident that it will always exist here because in this Webhook event, yeah, I think they didn't. You can see that when I hover over data here, they use the same asset object for all of them. So they didn't exactly modify how the asset will look like on different Webhook events, but we know how it's going to look like. But either way, we have protection in case it's missing here. Great. And now we have the thumbnail URL, which is cool. But one thing that we are missing is inside of our schema videos, we don't have the thumbnail URL here. So let's go ahead and go after mux track status. Let's add thumbnail URL. And I'm not going to prefix this with mux because we will be able to upload our URLs as well. Right. We will also be able to use AI to generate a thumbnail. Great. So when I added this, I'm now going to do Bonax DrizzleKit push just so the new thumbnail URL is pushed to my database. And then what I can do in the route here is I can update my video. So let's do await database update the videos schema dot set. and now let's update the MUX status to new data status because at this point the status will be ready so it would be nice to indicate that to the user. We will have the playback ID. We will have the MUX asset ID to be data.id and we will have the thumbnail URL like this and then we use where equals videos max upload ID matches data upload ID. It looks like data upload ID is also optional at this point so I'm going to check the same thing here. If we have no data upload ID there is no way for us to know which video we are supposed to upload modify so we're just going to say missing upload ID and a status of 400 here as well. There we go. And now we don't have to worry about that. Great. And let me just add a break here. And let me confirm, do I have a break here? Yeah. A lot of people prefer, you know, using this switch cases for webhooks. I don't know. I've always preferred if clauses. I always find them to be more readable. But yeah, okay, let's continue using the switch case here. Just make sure you added a break here. And I think that now this should work. Let's go ahead and let's try it out. Let's go inside of our video thumbnail component. And let's create an interface, video thumbnail props. And let's give it a image URL, which will basically be a string but it can also be optional in case it doesn't work in case we didn't do this in case the video is not ready yet so video thumbnail props image url and what we're going to do is the following we are going to check if we have the image url or we're going to use slash placeholder SVG like this. So by default, I think nothing should really change here, right? But let's go ahead and try this out now. So make sure that you have bun run dev all running. So you should have your ngrok running as well. So the mux webhook can communicate. And I'm going to add a new asset here. And I'm going to go ahead and upload my demo video. Now, I'm not sure if this will work on three tier, but we're going to see either way. So let me go ahead inside of my MUX here. I have my assets inside of my new tube development here. This is the most recent one that I've added here. 10 seconds. Okay. Playback and thumbnails. This is the playback ID, right? and I think that if I click on create thumbnails, they teach you how to do that here. For example, you just, this is exactly what we did on the backend, right? Image.mux.com and then we used the playback ID, which is exactly what we have here and then just thumbnail and then .png or something else. So I think this should work. Let's see. Looks like we got a few post requests inside of our webhook here. So that could mean that there is a webhook, that there is a video. Looks like I don't have this running. Let me just run my DrizzleKit Studio here. So that should now mean that in my database, I should have at least one video with a thumbnail URL. Let's see if that's true. There we go. Looks like one of them has a thumbnail URL. So let's go ahead and try this out now. I'm going to go ahead and refresh. uh maybe they're not ordered correctly but looks like no looks like it's not uh working i think i know why it's not working because we forgot one thing instead of modules studio ui sections videos section i never passed the image url to be uh video.thumbnail url and let's modify this to accept string or null. There we go. So no errors now. Let me refresh. All right. An error. An error is because image.max.com is not configured under images in your next.config.js. So just confirm that this is the host name you have, image.max.com, or if you have any other host name that okay You just have to know what your host name is And in order to fix We have to go inside of next And we now have to add images remote patterns protocol, HTTPS, and host name, imagemux.com. And the change of the environment will automatically restart the server. So you just have to wait a bit longer this time because this counts as a server reload. And let's wait a second. And there we go. You can see that we have automatically generated thumbnails coming from Mux. This is really, really cool. So from now on, for every video you upload, you will have an equivalent thumbnail being generated. Great. But that's not all we can do. We can also create animated GIFs, as you can see. For example, let's wait for this to load. You can see how this video is slightly moving, right? It's a very short GIF, but it is an animation. And we can do that as well. So let's go ahead and go inside of our schema here. And besides thumbnail URL, let's add preview URL as well. Like this. and let's go ahead and do Bonex drizzle hit push and then let's go back inside of our videos webhook here and what we're going to do now is we're going to create the preview URL in the same way so const preview URL will be the same thing HTTPS and this will be imagemax.com Again, playback ID, and this will be animated.gif like this. And then you can pass in the preview URL here as well. Great. So let's go ahead and check that out. So this is how we're going to do that. We can't really see this anywhere now. So what you have to do is you have to upload another video again. For example, I'm going to upload another video and I'm going to verify with drizzlekitstudio. Let me just find the proper command. So drizzlekitstudio, I'm going to go ahead inside of here again and I will refresh my videos. And, you know, when these webhooks hit again, that probably means that the video asset already has been added here. And I should now have a preview URL for at least one of my videos here. There we go. So what I'm going to do now is I'm going to force the code to display the GIF instead. So I'm going to go inside of modules, studio, UI, sections, video section. Instead of thumbnail URL, I will now use the preview URL like this. And let me just refresh this entire thing. and now you can see that this is a gif it is changing right you can see how you can see me scrolling in here great so that is a gif amazing and now this is what we're going to do we're going to wrap up our video thumbnail by making it behave properly so we're going to pass in for the image url the image url and then we're going to have the preview url video preview url and this is thumbnail URL, right? So we're going to have these two, but besides that, we will also pass some more things here. We're also going to pass the title, which will be video.title. Now let's go inside of the video thumbnail and let's modify the props. So it accepts those as well. So the title will be a required string. We're going to have the preview URL, which is going to have the same properties. And now we can extract the title and the preview URL from here. And now we're going to go ahead and do our little trick with the video thumbnail here. So this is how we're going to do it. We're going to give this whole element a group like this. And then what we're going to do for this first image here is the following. Let me just go ahead and collapse all of these elements here. In here, what I'm going to do is I'm going to set the, when the group hover is active, I'm going to set the opacity of this image to zero, right? So right now, if I hover, it disappears. And what I'm going to do is I'm going to copy this image element. This one will use the preview URL, and it can also fall back to the placeholder. That's fine. And I'm just going to reverse the logic. So when we hover, this will be fully visible. And by default, this one will not be visible. So now when I hover, it activates a GIF. So I can kind of preview what's going on. You can see, for example, this one does not have a preview. So it just falls back to our placeholder, like something's wrong, right? Because it doesn't have the preview. Great. And one more thing we need to do is the duration here. here. We need to kind of show a little box here in the bottom showing exactly how long this video lasts. So let's go ahead and do that. And also we can now change the Alt to both of this to be the title like this. There we go. So let's go ahead inside of our schema first, and we're going to go ahead and add a duration. So after preview URL, add duration, which will be integer from drizzle.pg-core duration like this. Make sure you have imported the integer from drizzle.rm.pg-core. And now we're going to go back inside of our API for videos webhook. And in the same place where we generate the thumbnail and the preview URL, we can now generate the duration. So we're going to do const duration will be duration, my apologies, data dot duration, question mark, math round, data duration times a thousand because it uses milli units to store the duration. Or if for any reason we can't load the data duration, we will just fall back to zero and passing the duration here. There we go. So now after a video has been processed, we're also going to know exactly how long the video lasts. So let's go ahead and go back inside of our video thumbnail here. And let's add a prop for the duration. So the duration will always be required. And what we're going to do is we're going to destructure the duration here. and we're going to go ahead here at the bottom where we added the to do and add a div here with a class name absolute bottoms to right to px1 py of 0.5 rounded background color of black with an 80 opacity text white text extra small and font medium And then inside of here, we're going to render the duration like this. So let's go inside a videos section here and let's render the duration to be video.duration. Now this will cause an error because duration can sometimes be not defined. So we can just set it to be zero as default. And now our, let's just see the errors. Column duration does not exist. Did I push my changes? I did not push my changes. Don't forget to push the changes so you have an error like I do. Let's refresh now. There we go. So now you can see that I have this weird zero number here. So let's go ahead and make this a little bit prettier by creating a format duration util. So I'm going to go ahead inside of my lib, inside of utils. right here, instead of utils. Oh, I have to go inside. My apologies, I was already open. Let's export const format duration, and let's accept the duration, which is a type of number. Let's get the seconds to be math floor. Use the duration. Use the modulus operator with 60,000 inside, and then divide that by 1,000. Let's get the minutes using a similar formula. Math, floor, duration, divided by 60,000. And then we can just return the string, how it's supposed to look like. Minutes, the string, add start to with zero in the string as the second argument. And then we're going to add a column. And then we're going to represent the seconds. So seconds, the string, add start, two, and zero as the string. Like this. There we go. So now go back instead of the video thumbnail, and you can import format duration, not from date FNS, from our local util, right? Make sure that's the one you import. and now it should have a nicer displayed duration. But still, as you can see, these previous videos of ours don't have any duration. So let's try this again. I'm going to go ahead and upload. And this time, I should have a proper duration from that video asset ready webhook. So now you can see I have no thumbnail here and the duration is 0000. But if I refresh, there we go. I can see the duration of 10 seconds here because MUX limits the video to 10 seconds. And I have the thumbnail and I have the preview URL. Excellent. So what we're going to do now to wrap up this chapter is the following. Let's keep track of everything we did. We updated the video schema. We pushed the database changes. We handled the video asset ready event. We assigned the thumbnail and the preview. And now we have to handle these two right here and these two right here. And we also, also what I want to do is I want to wrap up the UI on this part, right? There's nothing we can do about a few of these. Like we can handle the views the comments and the likes but we can handle the status and the date and some additional information here So how about we do that Let go inside of the videos section here. And what we're going to do, outside of this div, which encapsulates the video thumbnail, let's add a new div, which will have a class name, Plex, Plex column, overflow hidden, and gap Y of 1. And then inside of here, a span, which will render the video title. So now we have the video title here. But let's give this video title a class name of text small and line clamp 1. In case it's too long, it will collapse. Now you can go ahead and copy this span. This one will have a video description. Though the description is optional, so we are going to add a fallback here to no description. like this. This one will have text extra small, and it will have text muted foreground. There we go. Untitled and no description. Great. So now what we're going to do is we're going to find the table cell, which says status. And what we're going to do right here is the following. We're going to add a div with the class name, flex and items center. And inside, We're going to render videos max status like this. So now you can see that we have the status, which says ready, ready, ready. And this one says waiting because this one's never received their respective video, right? We didn't have the webhook ready at that time yet. But let's make this status appear a bit nicer. How about we go back inside of our utils here where we added the format duration. And let's add one more util called snakecase2title. The string it will accept will very simply be used for some regex replacement here. So add a forward slash underscore forward slash g representing global. Go ahead and add a space in an empty string. and then another replace forward slash backwards slash b backwards slash w forward slash g find the character and do character to uppercase like this and now that we have snake case to title we can go ahead and wrap this inside of that snake case to title like this make sure you have imported snake case to title from libutils. I will just move it here. Do I have an error? It seems like I have an error here. So snake case the title max status, which can be empty. So I'm just going to fall back this to be an error, right? In case we haven't managed to assign any max status at all, something's obviously gone wrong. But there we go. Now you can see we display a nice capitalized ready. And in case the status ends up being, because the statuses are stored like this, They will be, you know, video ready or video error or asset ready. So when we pass that through snake case to title, it will transform to video ready. Right. And we can just fall back to error in this case. Great. We can also handle the date while we are here. So let's go ahead and let's import format from date FMS. We've never installed this explicitly, but I'm pretty sure it came from Shatzian UI because we have a calendar package or I don't know, some other component which uses date FNS. In case you don't have it, you can just do bun add or npm install date FNS. Once you get the format from date FNS, you can go ahead and find the date here. And you can just use format, new date, video created at, and then pass in any format you prefer. Great. And I'm going to leave it at this. Let's just go ahead and do one more thing. So I'm going to give this table cell a class name of text small and truncate in case it goes too far. But yeah, I think these are all the information we currently are able to give to the user. And we can also store the visibility while we are here, just so we can wrap up everything that currently our data allows us to do. We can't do views, comments, or likes, because that requires a whole another database entity. But we can do visibility, we just don't have it yet. So let's go ahead and do visibility. In order to do that, we're going to go inside of our schema. And we have to create above the videos an enum. So export const video visibility, pg enum, video visibility. And you have to import pg enum from pg core like this. And then just open up an array and add private. or public like this. And now what we're going to do is we're going to add the visibility here, which will be video visibility. Default will be private and it will be required like this. And if you want to, for the duration, you could also do the same thing. you can set the default here to be zero and also make it not null. So it's kind of always required if you want to do it that way. I would not recommend doing the same thing for thumbnail and preview URL simply because we are using local relative paths here, right? We are using slash placeholder.sbg. So if you were to do inside of your schema here, for example, a default of that, I'm not sure that's a good practice, right? I think you should aim at having it at least be hosted somewhere like this, maybe, but we don't even know what our domain is going to be or anything yet. So I'm just not going to do that. And in my original source code, I didn't do this either, but you know, if you want to explore for yourself, maybe this is a solution you prefer rather than fall backing to zero here. I think that's the solution I prefer as well. So this is what I'm going to do. I'm going to go ahead and see, can I, my drizzle is not working. All right. So this is what I'm going to do. I'm going to do Bionic's drizzle kit push, and I probably will get some conflicts now because I have a bunch of data here. Yeah. It's throwing me some errors here. Uh, the column duration of relation videos contains null values. Okay, so this is what we're going to do then. I'm not going to add the duration. My apologies, I'm not going to modify the duration. I will just add the video visibility here. But I think that might also throw some errors because my other video visibility does not exist. Oh, no, that's different because there are no conflicts, right? None of them have the video visibility. So I think that all of them will now receive video visibility. Let me just open my drizzle kit studio. Nope. Like this. So I think that all of my videos now should receive the video visibility. Let's see. And all of them should be private. There we go. So all of them just got the visibility to be private. So what I'm going to do now is the following. I'm going to go ahead and select all of my videos and I'm going to delete them. And you have to do this as well if you want to modify the duration. If you're fine with this solution, okay, you don't have to. But let's go ahead and add a default of zero and not null. So I pretty much always expect the duration to be something. And let's go ahead and do this inside of our route for videos. We also have to be careful then. Okay, it's okay because we always fall back to zero. If something's wrong, we will just fall back to zero. That's fine. It's just important that this can never be null because then this will error. Great. So now let me just confirm instead of my schema, I have modified this to be not null. So what I'm going to do is I'm going to do drizzle, hit push. So let's ensure that that will work. There we go. Change is applied. Great. And now let's go inside of the route and let's just wrap it up by adding some more events that we need to handle. So a very simple one here will be if the video has errored. So very similarly to case video asset ready. After the break here, we're going to have case. I apologize here. Case video asset error. Let me just confirm. Did I do this correctly? Yeah. Okay. We can also add space between these if that makes it easier for you to look at. I really, really prefer if clauses over this. All right, so now we have the data element as well. Payload data as video asset error and get the data inside. We can go ahead and do the same thing that we did above. If we have no upload ID, we have no idea what the video has actually errored. So no need to even try something. And then let's just do awaited database update the videos, set max status to be data status here, which will most likely be an error, where equals videos upload, max upload ID is data upload ID. And let's break. Great. We have the video asset errored. And now let's go ahead and let's do case video asset. And I believe I did not add this one. So video asset deleted webhook event. So I'm going to go ahead and add this as well. Video asset deleted. This is going to be particularly useful while you are in the free tier where videos are deleted after 24 hours So you won have mismatching data where you know one thing is deleted in MUX but available in your database If something deleted, if after 24 hours, the video gets deleted in MUX because of the free tier, it's also going to be deleted on your backend, I mean, your database. So there are no weird states here. So we can do the same thing as we just did above here. I think I can copy this entire thing. this will be not errored, but it will be deleted. There we go. And then we can do await database delete. What's the argument here? Videos. Where equals videos max upload ID is equal to data upload ID. it's not camel case it's pascal case like this or snake case my apologies all right so that's if the video asset has been deleted and i'd like to immediately try this one out i want to see if it's working so i'm going to do bond run uh dev all here and i'm going to go ahead and have my assets here make sure you are inside of the proper project. And I really do want to clear them all out. So what I'm going to do is I'm going to delete them one by one. And since I recently deleted all the videos inside of my database, I wouldn't be surprised if the webhooks are failing now because it should not be able to find any of these assets related, right? So let's see. It looks like it did hit my webhook a lot of times, no errors, which is, well, I guess, okay, right? Because this basically just does its job. It cannot find anything and that's it. So let's see, does this work or not? I'm going to add a little console log here. Console log, deleting video, upload ID will be data upload ID like this. Let's try it out. And let's add the equivalent creating video. How about we do that in the video asset created? We're going to have creating video and we're going to have deleting video. So let's go ahead here. Let's refresh this. We should have no elements here because we cleared them all out with our studio. Let me click create, select files. And now I'm going to keep waiting here. There we go. Creating video with this upload ID. That's great. I have my Drizzle Studio open. So I'm going to refresh the videos here. And I think that now by default, I should have, yeah, I should have refreshed sooner. But the one thing we changed is that my duration was zero in the beginning. But great, we also have the visibility and the duration here, the preview URL, the thumbnail URL. These two fields are still missing, so we will do that as well. But great, we definitely have this one video here. And if I refresh, I should see all the updated information here. Sometimes when the GIF is not loaded yet, when you hover, you're going to see an invisible image because it's still loading that GIF. So now let's go ahead, instead of the assets here, let's refresh. as always ensure you're in the proper project and let's try and delete a video so this is something that should happen after 24 hours and I'm hoping that it will also you know fire a web hook as you can see this works deleting video by upload id and I think that if I refresh here there we go no video here perfect so we fully synchronized our assets on the mux and our assets for our database. Excellent. That works. And I'm seeing a little warning here. The requested source, which is our animated GIF, is an animated image, so it will not be optimized. Consider adding the unoptimized property to image right away. Let's go ahead and type our video thumbnail. This is the place where we use the preview URL, and we're just going to add an optimized. And we can, in fact, only add it if we don't have the preview URL, because otherwise it's just going to be a placeholder. So if we have the preview URL, we will mark this as unoptimized. Great. And then we should no longer be receiving this warning here. Great. That works. And now the last thing we have to do is the track error. So we handle this, we handle this, we handle this, we handle this, these two are left. Now, here's the trick. This will only fire if you have audio inside of your videos and also if you enable the subtitles in the first place. So we're going to try this now. I'm going to go ahead and I'm going to, I forgot to break here. And I'm going to add video asset track ready. So that's what I'm going to add. I never know how to write these cases. Okay. Let's go ahead and get the data here. There we go. This will be video asset track ready lab hook event. We should do what we always do, which is check for the upload ID. by oh the upload id does not exist on type track okay so by this by the time the track is ready i'm pretty sure that the video asset has already been completed which means that we have the asset id stored so we no longer have to rely on the upload id i think we can use the asset id uh let's see what do i have here i'm gonna have to take a peek at my source code to remind myself of what is the proper way of solving this. Okay, so the asset ID comes from the following, data asset underscore ID. But here's the trick. It says that the asset ID does not exist on type FRAC. And I can see in my original source code, I added a little comment saying that that's not true. Asset ID exists in here, But for some reason, they put a TypeScript object here, which says that it doesn't exist. So what you can do is the following. You can add and asset underscore ID string. And you can add a little comment here. TypeScript incorrectly says that asset ID does not exist. Like this. And let's also assign our track ID, which will be data.id. Let's also get our new status, which will be data.status. And then what we're going to do is in case we're missing the asset ID, we're just going to throw an error, missing asset ID. Great. And now what we can do is we can go ahead and simply update the video. So await database.update videos set. Mux track ID will be our new track ID. Mux track status will be our status here. Like this, where equals videos. Mux asset ID matches the asset ID. And break. All right. So now we should also handle this. And I'm now going to add a console log here, track ready. The reason I'm adding this here is because I want to see this track fire, but I'm 90% certain that it's not going to fire now. I'm going to add some space here and I will upload a video here, demo.mp4. So this video has audio, but I think that since I've never enabled transcription, it will never have the asset track ready. Keep in mind that the asset track ready does take some time to fire. It's not as fast as our other webhooks. So I'm going to give it some time, but it's a 10-second video. I'm pretty sure that already it should have been here. So this is what we're going to do now. If you actually go inside of your videos here and just look at your new asset, you will see that it doesn't have the subtitles function here at all, right? So you have to, you can retroactively add subtitles, but the proper way of doing it from the start would be going back inside of our videos module, our procedures here, right here in the upload, in the new asset settings. So what we have to do here is we have to add below the playback policy, add an input like this, generated subtitles, language code will be English and the name will be English. So that's why I told you if you can find any English audio because then you will be able to have generated subtitles. So now what happened is that my track ready event never fired. But I'm pretty sure that now when I add another video here, it's going to transcribe the video. So I'm going to have it uploaded and let's wait. So it created the video and let's just wait for some time. Hopefully I did this correctly. And there we go. Now we have track ready. What does that mean? And well, now if you go into your assets and select your proper one and go into the newest one, and if you go ahead and click play this time, you can see that you now have subtitles, right? I'm just going to mute this so you don't have any audio feedback, but you can see the subtitles here clear as day, which means that the track is ready, which in turn should that inside of my Drizzle Studio, I should now have two records. One which doesn't have the track and the other one which has the track. There we go. One has the track status as ready and the other one does not have it at all. Perfect. So that is it for the webhooks. I will come\nback a bit to this track status. I mean, this track ID, this is something that we will need, but the track status, it's not exactly something that we need other than displaying to the user, like, hey, the captions will come in a moment. They are still being processed. But if you upload a video which has no subtitles, in that case, I'm pretty sure that this event will never fire, right? So we will explore if we can improve the behavior on that part. So let's just wrap it up by properly displaying the visibility since both of them now have their visibility. So we can go back to our videos section for that and let's find the visibility table cell. Let's add a div with a class name flex and items center. If video visibility is private, in that case, we're going to render a lock icon from Lucid React and give it a class name of size 4 and then R of 2. Otherwise, we're going to render a globe 2 icon from Lucid React with the same class name. And then below that, we can use snake case to title video visibility. Even though we know there's only two options, but why don't we reuse our util here? Make sure you have added the lock icon and the globe icon from Lucid React here. So now, there we go. You should see this saying private, or if it's public, it will have a globe icon and it will say public. Great. So we now have the nice preview. We even have subtitles, which is not something we can see yet. We can see the duration of the video. We have completely synchronized what's going on with our 24-hour deletion. basically everything is now synchronized in a nice way. But still keep in mind you know you might forget that the videos are deleted after 24 hours so always have a small number of videos like one or two so you can easily detect that. Don't have like 50 videos and then you try and demo it to someone and everything's broken because the videos have been deleted. But keep in mind you can if you have the ability to add a credit card you can just click upgrade. Make sure you select the pay as you goat here, and you will be able to finish this tutorial completely for free. I didn't manage to spend a single cent, and you will have full benefits. You will have everything unlocked. I think that's simpler if this is causing any problems for you. Great. I think we can now finally check two of this as well. Great. We finished all the MaxiWeb hooks that we need, and what we can do in the next part is we can create a proper form and some video player so we can actually see this video in our app. Great, great job. 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 dropdown 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 going to go back inside of my videos section. It's actually right here inside of the studio module. And I'm going to 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. So 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 want to 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've 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 a 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 write. 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 write, to my actual elements here, which I believe are views, comments, and likes. So all of these should have, whoops, last name of text write. 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. 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 going to have to create a proper prefetch method here. So let's go back inside of studio procedures here, and we're going to develop a get one method. So we have get many 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 wanted 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 here simply because I know that in my schema I use video uuid. In your case, you can just put z.string. It's going to work just fine. 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 dot select. And passing from videos. Inner join. My apologies, no need for an inner join here. We're just going to pass in where. And then we're going to add end. Because we need two things to happen. And 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 video's 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. So in case there is no video, we can go ahead and throw new ERPC error, which we can import from the server, passing the code not found. There we go. So we now have our get one procedure here. So how about we go inside of the page here and we prefetch that. So let's mark this as an asynchronous page. let's not forget to export const dynamic force dynamic because we don't actually await anything we just prefetch so nextjs 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 pre-fetched our video here great so what we have to do now is we have to hydrate the clients. 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 going to 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 going to resolve that, but let's first create the video view dot DSX. And inside of here, let's just add page props here. Video ID will be a string export const video view video id page props so these will already be awaited so no need to do it here again and 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 going to 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. Inside 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, video view here. So we're going to create a new section, which will be called formsection.tsx. And instead of this form section, we're going to mark it as use client. Expert comes to formsection. The form section will have page props, right? You can call it formsection props however you want. It really doesn't matter. So we only need the video ID here. And basically what we're going to 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 from 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 prefetching rules. It would mean that we are fetching something incorrectly. Let's go ahead and confirm. So we are calling TRPC Studio get one. We're calling use suspense query with the ID of video ID. And inside of our page here in the studio, videos video ID page, your PC 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, 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 many 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 flex items center, justify between and margin bottom of six. And inside of here, I'm going to add another div, which will simply hold my heading video details. 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 two 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 how that going to look on widescreen We are not going to center it We 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 want to do is I want to 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 going to go next to this button here, and we're going to 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 drop-down menu trigger and add a drop-down menu content here. with an align 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 uh drop down here and 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 Shatzi and 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 Zod resolver, which is again the same thing that we got when we installed everything from ShatCN, because when you add form component from ShatCN, 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 are 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 use form 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 use form return, 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 DrizzleZod 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 going to 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 constant video select schema just in case we ever need it, right? It doesn't hurt to have one. And pass in 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 sod, 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 on submit 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 element, right? So this is how we're going to do that. We're going to create a div and we're going to create a grid. On mobile, this will simply be a single column. But on large devices, we're going to turn this into grid calls 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 8. And on LG, a call span of 3. 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. 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 titled. 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 going to 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 ER10. And 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 going to limit the number of rows to 10 so it doesn't look too big nor too small. And we're going to 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 to 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 have 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 going to 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 on value change to call field on change, and default value will be field value or undefined. Then inside of form control, we're going to 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 get many. 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 prefetching the video, let's go ahead and prefetch categories. There we go. So now let's go ahead back inside of our individual form section. and we're just going to do the same thing. So we're going to 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 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.net, 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 going to 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 backend form validation. I really like how easy it is to do that with trpc and with use form. 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 the entire input, but I want to 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 I 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 ad. And now let's just add a where. It has to use and. We're going to have two equals here. Looks like we have to import both and from Drizzle ORM and equals from drizzle.org. So I will move that to the top here. And the first one will be checking if our video's ID are matching the input ID. And the second one will be if video's user ID match the user ID, which we got from this protected procedure. And let's get back returning. In case we don't have updated video, we're going to throw 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 onSubmit 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 want to use that field, you have to mark this as asynchronous. So depending on how you want to 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 going to 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 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 going to add TRPC, my apologies, utils, dot, the one I want to invalidate is studio, get many. dot invalidate\nStudio, why is it not working? Studio get man, this expression. Oh, I should not call it. So I just want to invalidate it. And utils, studio get one, invalidate, specifically this one. 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. You know, 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 the RPC, you know, 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 a change hello to hello123 and click save and go back here. There we go. Hello, one, two, three. 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 going to 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 eight, and enlarge call span two. 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 live 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 max playback ID and poster URL, which is video thumbnail URL like this. Actually, we can rename this to 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 autoplay so that we can decide when we want to do that or not. And we are also going to have this on play method here, because later on, that's how we're going to count the views after the video plays, not just by a website visit. So let's go ahead and export const video player. 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 muxmuxplayer. So let's go ahead and do that. Bun add, add 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. One add max max 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 max player. Max 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 PlayerInitTime 0 This helps with hydration. If you don't set this, you will get some hydration errors. Autoplay simply to match our autoplay boolean ThumbnailTime 0 ClassName FullWidth, FullHeight and ObjectContain accent color. For this one, you can basically choose what your 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 form 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 uh let me try and speed it up by closing this and running one 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 double audio, but we can finally preview our player here. We have captions. We have everything here. It 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 going to do is below the video player, outside of its div, we're going to add a new div with a class name, px4 and py4. So basically just adding four, yeah. Flex, flex column and gap y of six. Now we're going to go ahead and add another div. So we're 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 going to add a div with a class name, flex item center and gap X of two, 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, one, two, three. Let's give this a class name of line clamp one, 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. OnClick will be an empty arrow function for now and disabled will be false for now. So 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 backticks and we're going to use either process.environment.verse Vercel URL or HTTP, whoops, HTTP localhost 3000. And then we're going to go to slash videos and then video ID. So that's the full URL. I'm going to 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 going to 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, right? So outside of this button, outside of this div, outside of this div, and outside of this div, right here, we're going to 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 1, 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 mux status. Max status can be empty. So let's default to preparing. And let's give this a class name of text small. Import snake case to title from our lib utils. 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, you know, it's going to 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 if the video doesn't have any audio. 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 going to 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. So 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 globe to icon from Lucid React with a class name, size4mr of 2. And in here, you can add a lock item. 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 this need a div with a class name, flex items center. 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 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 uh 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, 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 on success, which we never actually use. So how about we go ahead and use it now. So on success, the following will happen. On success. First things first, we're going to 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. 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 getMany, But what we can do is actually no, no need to do any of those. 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. to go to slash dot push slash studio videos, create data video dot ID. 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. 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. 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. 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 mile-apology server procedures. And we're just going to add delete here. Again, protected procedure. Input. And inside of here, we actually only need one thing, which will be the ID, z.string.uid. Let's import z from zod. Now let's add the query. 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 do const deleted Yeah here the thing I don 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 AND and to equals inside. First one, we'll 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. 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. 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 update. 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. 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. Use mutation. Oh, procedures. Did I do something wrong? Oh, I did dot query. it should be dot mutation. There we go. So now use mutation should have no error. And now let's use this inside of our dropdown here. On click, remove, remove dot 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. 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. Let's get our handy check mark 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. In this chapter, our goal is to create video thumbnails, which we can actually upload. So far, we've only had thumbnails which were generated using the playback ID coming from Muck's webhook. We're going to use Upload Thing as our upload service. And this is the UI component which we are going to develop. So this component will be right here above the category. And once clicked, it will give the user options to either upload a thumbnail or to restore the thumbnail to its initial state, which was the MUX webhook one. And we're also then going to refactor our thumbnail fields in the schema so that we can have a proper upload thing cleanup. Let's start by integrating upload thing. So head to uploadthing.com and go ahead and create an account. Once your account has been created, let's create our first application here. I'm going to call this application new tube like this, and I'm going to select the free tier. I'm not going to fill any of these options. I will just click create app. There we go. Now let's go ahead and let's click on API keys here. So it looks like we only have one key, Upload Thing Token. So I'm going to go ahead and click Copy here. I'm then going to go ahead inside of my .environment.local and I will add it here. As always, don't share this with anyone. Now let's go ahead and let's click on the docs here. so we can see the proper way to add this to our project. I'm going to select Next.js app router, and I'm going to follow the instructions for BUN. So we have to add Upload Thing and Upload Thing React. I'm going to go ahead and add these two, and then I'm going to show you the versions. So 7.4.4 for Upload Thing and 7.1.5 for Upload Thing slash React. We already added the environment variable, so that's fine. Now let's go ahead and let's set up a file router. So we're going to go ahead and create inside of the API folder, upload thing, and then core.ts. Let's go ahead inside of source app folder, API, new folder, upload thing, and then inside core.ts and paste the file inside. Inside of here, you will have a very basic mock authentication method as well as the basic image uploader router here. We will later modify this entire thing, but we can leave it like this for now and you can ignore the type error here. After you've established the core.ts file, let's go ahead and create a route.ts. So I'm going to go inside of upload thing and add a route.ts and add it here inside. Inside of here, we are importing our file router from core and we are assigning it right here. Let's save this file. And now let's go ahead and let's create the upload thing components. So I'm going to go ahead and copy this. And you can choose where you want to put this. Let's put it inside of source utils upload thing simply so we stay true to the documentation. You can, of course, move it to your place of preference later. Let's go ahead and add upload thing dot TS inside of our lib folder. And let's put it here. We now have to modify this to use an add sign because that's the alias that we use. There we go. We now have upload the button and upload drop zone, which has strict types to our file router here. So later when we define thumbnail uploader, we will know exactly what we have to pass from the front end here. And I think that that's it. all we have to do now is add this wrapper around our Tailwind config. And I can see that they have actually added Tailwind version 4 here, which is interesting because Tailwind version 4 just came out. They are very fast. So in our case, we are using still Tailwind 3. I'm not sure if, can we see that anywhere? Tailwind, yes, Tailwind CSS 3. So we have the config file here. So this is what we have to do. We have to go inside of tailwind.config.ts. We have to add with upload thing and then we have to export default with upload thing and wrap the entire configuration into that. So go to the end and end the wrap here. And this should still satisfy the config. And yes, we still have this require error, but again, it will not interfere with our app at all. What we have to do now is we have to mount a button and upload. Well, we can already try that, but we can stop here. I mean, this is everything we need for now. If you want to, you can continue reading. But what I'm going to do is I'm going to go back inside of my dashboard. so that I can actually see my files being uploaded here. So what we're going to do now is we're going to go ahead and start working on this component right here. So let's go ahead inside of our form section here. And what we have to do now is we have to create those components, which I was talking about, right here to do add a thumbnail field here. So that's going to be a form field in itself. And now let's go ahead and give the form field a name of thumbnail URL. Let's give it control of form.control. And let's go ahead and give it render. And let me just move render here. So I'm going to destructure. Actually, we don't need any field here because, well, technically, this is a form field, but we're not going to use it exactly the same way we used our previous ones in a sense that it's not going to need any kind of form control because all that this is going to do is it's going to open up a model and display the current items. So let's add form control here. Let's add a div class name padding 0.5 border border dashed border neutral 400 relative height of 84 pixels, width of 153 pixels, and group like this. So I think I'm missing a little space here, maybe. There we go. Yeah, make sure you put space between the width and the height. And then inside of here you going to use an image from next image So just make sure you have added an import for that And we're going to give this fill. Alt will be thumbnail. And we're going to have a source, which will either be video.thumbnailurl or slash placeholder svg. And let's also give this a class name of object cover. There we go. So now you can see my current thumbnail right here. One thing I want to do before we move forward is standardize this. We are using it in three files in four places. So what I'm going to do is I'm going to go inside of videos and I'm going to create types.ts here. Actually, not types. I'm going to do constants. dot ts in the videos and I'm going to export const thumbnail fallback and I'm going to add slash placeholder svg here and then I'm going to search for placeholder svg again starting with our form section I'm going to go ahead and fallback thumbnail for fallback here, which was auto-imported from modules videos here. I'm going to go ahead and continue searching. Next one is inside of videos UI components video player. So let's go ahead and use the thumbnail fallback here from dot dot slash dot dot slash constants. And the last one is in the video thumbnail component. again, inside of the videos module, UI components, video thumbnail. So two places here where we are going to modify to use thumbnail fallback from dot dot slash dot dot slash constants. And if you want to, you can also, you know, be consistent whether you want to use double pipe or double question marks. So let me just see all the places where I use it. Inside of form section, I'm also going to replace it here. I think there's no need for double question marks here. There we go. Now I'm consistent in all of the places where I'm using this. Great. Let's focus back on our form section here. Nothing should change as of now. This should still work as expected. But what we're going to do now is just below the image, we're going to add a drop down menu. I believe we already have the drop down menu because we have used it to generate our delete button. Inside of here, let's add the drop down menu trigger and give it an as child property. Let's add a button component which will have more vertical icon. We already have all of these imported it seems. Let's give this a size 4 and text white. Actually, size 4 is not required explicitly. Let's go ahead and give the button a type of button. This is very important. A size of icon. And let's go ahead and give it a class name of background black 50. On hover background black 50 as well. And let's also go ahead and do the following. Confirm that inside of this div, you've added a relative class so that then we can safely give this button an absolute class and position it like this. And now you should see a little three dots here. It's kind of hard to see how this looks like because the background is actually black as well, but it looks, well, it looks okay. Not sure where I was going with that. Okay. Now let's go ahead and do the following. Let's make hit fully rounded and let's give it an opacity 100 on mobile. But when we hit the medium breakpoint by default, this button will not be visible. Only when we do a hover, will it be visible again. And the group will be this div as well. So this div needs to have group and relative in order for this button to work. I mean, you don't have to have all these styles if you don't want it. And let's go ahead and give this a duration of 300 and a size of seven. There we go. So now only when I hover does it actually show the little more vertical dots here. Great. So now let's go ahead and let's add the drop down menu content. Give it an align of start. and the side of right. Open up the drop-down menu icon. And in here, we're just going to have change with image plus icon. Give this a class name of size 4 and MR of 1. Make sure you have imported the image plus icon from Lucid React. So now, when you click here, you should have the option to change a thumbnail. And this will actually open the upload model, which we are going to create. So what I want to do now is I want to copy this drop-down menu item two more times. So we should have three options for the thumbnail. The second one will be AI generated. And the last one will be restore. So we should have these three options. And now let's add respective icons for each of them. The AI generated will use the good old sparkles icon. You are probably used to seeing this icon everywhere by now when it comes to AI related things. And the last one will be the rotate CCW icon from Lucid React, of course. There we go. Change, AI generated and restore. So now today, I mean, in this chapter, we're going to implement the change and the restore one. And we're going to leave the AI generation for later because we will do all AI generation in one chapter, which will also be for description and for the title. So let's go ahead and focus on the change one. In order to do that, we first have to implement the thumbnail upload model. So let's go ahead inside of modules, studio, components, and create the thumbnail upload model dot TSX. SX like this, thumbnail upload model. And let me just, I have so many things open. I will just close all of them and go inside of thumbnail model open. Let's go ahead and let's import responsive model from components responsive model. And let me just copy the interface so you don't have to see me write it out. Basically, we need a video ID for the thumbnail, for the video, which thumbnail we're going to change and then your normal open and an open change states. Let's export thumbnail upload model. And then let's go ahead and let's destructure the props. So video ID open and on open change. And inside of here, what we're going to do is we're going to open a responsive model. I'm going to add a paragraph, say hello. and in here we are going to give this a title of upload a thumbnail and open prop as well as an open change prop like this and now let's go ahead and actually use the thumbnail upload model so let's go back inside of our form section here let's go to the beginning here and let's encapsulate the entire thing inside of a fragment like this, and you can indent the whole thing inside. And then we're going to render the thumbnail upload model, which is going to come from dot dot slash components thumbnail upload model. So now let's go ahead and let's prepare some states for this. I'm going to add that here. So we're going to have thumbnailModelOpen and setThumbnailModelOpen to use state and false by default. And how about I move... Okay, now this can stay here. It's fine. Let's go ahead and use those now. So we scroll here to where we render this. We give it an open of ThumbnailModelOpen. and on open change will be set thumbnail model open. And video ID will be our video ID like this. And then let's go ahead and find our dropdown here. This one with the image plus icon and change. And let's give it an on click of set thumbnail mode open to true. There we go. So now when I click on change, I have an upload thumbnail model. So what we're going to do now is we're going to go inside of thumbnail upload model. And we are going to revisit our upload thing here. So our upload thing lib. And in here, we have the upload drop zone. So let's go ahead and render it inside. Upload drop zone. not from the package itself, from our abstraction over it in the lib like this. It's going to be a self-closing tag and it needs an endpoint. And it's going to give you the option for the only endpoint it knows, which is defined inside of our API upload thing core right here. So similarly to TRPC, they have very good end-to-end type safety. That makes sense because the author of upload thing is also the author of the T3 stack, which uses TRPC. Great. So now, there we go. We have a nice little upload a thumbnail drop zone here. Now, obviously, if you go ahead and try and upload something here. So I just have prepared some random file. Feel free to use any image for this. It really doesn't matter. And if you go ahead and try now, let's see what's going on. Upload completed for user ID, fake ID, and we have the file URL. So this should mean that now in here, if I refresh, let's see, there we go. Instead of my files, I have a thumbnail.exe.\npng right here. Excellent. So what we have to do now is we have to go inside of core.ts, because remember, we have this video ID, but we have no idea for which video are we actually uploading a thumbnail. So this is what we are going to do now. We're going to go ahead and remove this fake out from here, and we can remove all of these comments here. Let's go ahead and do this. No need for any of this. So we are simplifying this by a lot. And let's change this to not be an image uploader, but instead a thumbnail uploader. We can leave the same rules for the image, 4 megabytes, maximum file of 1. That's fine. And now, similarly to TRPC procedures, we can use Zod. make sure you just import it we can use zod to add any input here so object required video id there we go and now inside of here we are not going to need to destructure the request but instead we can destructure the input like this and what we're going to do is we're also going to do a wait out, but our out here will simply come from clerk next JS server like this. And this will just have user ID here. If we don't have user ID, we can throw new upload thing error unauthorized. And then in here, we can return back the user ID, which we just got from the out function. And also we can just spread the input like this. And then in the on upload complete, we can go ahead and do the following. We can do await database, which you can import from s slash db dot update videos, which you can import from the database schema and simply set the new thumbnail URL to be file.url where equals videos ID metadata video ID like this and import equals from drizzle ORM. One thing that's missing here and which we can already fix is by importing and from drizzle ORM and let's ensure our usual two queries when it comes to updating the record. So first, what needs to match is this metadata video ID. The second is videos user ID, metadata user ID. But I can already see this is not going to work. Let's go ahead and be more specific here. This is clerk user ID. So let's go ahead and check it like this. We're going to pass the clerk user ID here. And this is what we can do. We can do const database user or just call it user and do await database. Select. My apologies, just select from users. Make sure you have users imported from schema here. Where equals users ID. My apologies, clerk ID is equal to clerk user ID. if we can't find this user in our database it means that uh well there's a there's something wrong with our web hook synchronization right if clerk user exists but not the user in our database something's wrong we can't really do much with this user at that point so we're going to stop that here and we're not not going to uh bring back clerk user id we'll just uh bring back the entire user here. How about we do that instead? Great. And now we have user here, so we can use metadata.user.id. There we go. And you can return whatever you want. I think we can return back the file. Can we? Looks like we cannot. All right. So I'm just going to return back uploaded by metadata user dot ID, right? So we have to check if user ID is the user ID from the database, not just the clerk user ID. That's why we need to do this first and only then allow someone to change the thumbnail URL. There we go. So now let's change this to be thumbnail uploader. and then we need input here and passing the video ID. There we go. And then we can decide what to do on upload complete, on client upload complete, right? And what we're going to do here is the following. We will get the utils from trpc from the client. Make sure you don't accidentally import from the server. trpc.useutils. and then we're going to develop const on upload complete. We're going to close the model by calling on open change and set it manually to false. Let me just fix this. And then utils studio get one dot invalidate for the ID video ID like this. and let's go ahead and pass that here. If you want to, you can also re-invalidate Studio Get Many in order to immediately, you know, call that, in order to immediately see the new thumbnail in the list of your videos there, right? I mean, eventually it will get re-invalidated by itself, But if you wanted to immediately see it, you can invalidate this as well. Let's go ahead and try it out now. So I'm going to go ahead and select change here. And I will upload my demo thumbnail. I will click upload. And after this, it should close. And it looks like we have an error. And that is because we are using a new image source. And we did not add it to our host names. So let's go ahead and confirm that the host name is utfs.io. If yours is something else, okay, just, you know, you have to read what it is here. And then you have to do the same thing we had to do when we used a MUX image. So go inside of next.config.ts and you can copy this object for MUX and simply replace with utfs.io. This will now restart the entire server. So you can hit refresh and you will wait for some seconds, but then it should show you the new thumbnail. Hopefully, there we go. Demo thumbnail is showing up right here. Perfect. So we have that finished. What I want to do now is I want to implement the restore method, and then we're going to have to talk about something, and that is file cleanup, right? Especially after restoring or after uploading a new file. How do we get rid of the old ones and make sure they don't unnecessarily populate our storage? In order to do that, we're going to have to refactor some thumbnail fields inside of our schema. So let's go ahead and do the restore method. So the restore method will be purely done inside of the videos module, specifically inside of its procedures right here. So what we're going to do is we're going to add restore thumbnail procedure, which will be a protected procedure. I think that we can copy literally, you know, exactly as it was below. This is going to be asynchronous and this will be an arrow function like this. And inside of here, we can destructure the context and the input. As always, we can get the database user here using context. And then we are just going to go ahead and reset the thumbnail URL. And we can do that by first getting the video. So let's do existing video. And let's do await database. Select from videos where we're going to use and. and then two equals inside. The first one will be if videos.id matches the input ID. The second one, videos.user.id, matches the user ID which we destructured above. If we cannot find the existing video, we're going to throw new trpc error with code not found. and otherwise let's go ahead and let's create a new thumbnail url which is very simply going to be the same thing that we did in our videos webhook so you can go ahead and find video asset ready here and you can just copy this there we go so just go ahead and add this here and let's also do this. How about we also check if existing video has no max playback ID. In that case, we can also throw new trpc error because there's nothing for us to fall back on. So we will just throw a bad request here and then we can go ahead and use this inside of here. There we go. And then let's go ahead and do updated video here to be await database update videos set the new thumbnail URL here where let's go ahead and do double equalization just in case. So videos ID matches the input ID and our videos user ID matches our user ID and returning. And then we can return the updated video. There we go. So that is our restore thumbnail procedure. We can now go back inside of the form section here and we can now add it here. So I'm going to go ahead and go find one of my remove procedures. For example, let me just copy it here. I will call this one restore thumbnail TRPC videos restore thumbnail So what we going to do here is we can invalidate many We can also invalidate one By passing in the video ID. ID, video ID. Like this. There we go. And in here we're going to say thumbnail restored. and we can remove the router push here. This can stay as something went wrong. And now all we have to do is use this. So let's go ahead and go find our dropdowns here. There we go. So restore. Let's give this a one click. Mutate and pass in ID, video ID. There we go. So now if I go ahead and click restore, it restores the thumbnail back to its original position, right? Because we are using the exact same thumbnail URL string as we did in the web hook. There we go. Perfect. So we did the following. Let's go ahead and mark it. We integrated the upload thing. Let me increase the opacity. We integrated upload thing. We added upload functionality. We added thumbnail restore functionality. And now we have to take care of proper upload thing cleanup. Because at the moment, you can see that now I have even more, you know, even less reason to hold these two files. They should have been cleaned up by now. So you can clean up files by their keys, right? Now, if I click copy file key, you can see that let's go ahead and test this out. So it ends with MR. And if I go ahead and open this URL here, and let me just enable this developer mode, this part you can't see, but this last part in the URL seems to be equivalent to the file key, right? MR. But in case this changes in the future or the URL structure changes in the future, I don't want to rely on the URL. So what we are going to do is the following. We're going to go inside of our schema here. And we're going to go ahead and alongside the thumbnail URL, also add a thumbnail key. And make that a text. and we can even do the same thing for the preview here so preview key you're going to see why i'm doing this in a second so yes technically what we could have done is created another entity called file which would have its url and key fields but for the sake of simplicity i kind of feel this is okay. I feel like it would be premature optimization to work with a whole another relation in my database and it would just unnecessarily complicate things. You can of course decide for yourself but I will choose this option. So I will add the key here and the key here. I will then do Bunnix drizzle kit push. Let's just confirm that I have everything I need here. There we go. Changes apply. And now things are going to be a little bit different, starting with my uploader here, the thumbnail uploader. So what we're going to do is we're going to store alongside the thumbnail URL key like this. But that's not all that we're going to do. Now that we know that we're going to have keys, we can leverage server-side API from UploadThing. So let's go ahead and do that. I'm going to go ahead and use from UploadThing server, UT API. And then I'm going to go inside of my middleware here. I'm not exactly sure if this is the correct place to do this, but I feel like we can. I mean, okay, we can also do it on the on upload complete. I would rather do it in the middleware because I don't even want the file to be uploaded if the old one is not already deleted. So this is what we are going to do. Since we have the video ID in the input, what we can do after we fetch the user is the following. We can check if we have that video, which we are trying to update. So await database, select from videos. I believe we already have the videos schema. And let's pass in two equals here. One will be videos ID. And inside of here, we're going to have input video ID. The other one will be video's user ID, and this will be our user.id because we do get the new user from the database here. And we can just select the thumbnail key like this. Is this how I do that? I forgot how you select things in Drizzle. Just a second. You use this key, my apologies. So videos thumbnail key. That's how you select a single item. So we only need this, right? I don't need any more information. So what I'm going to do is first check if there is no existing video. In that case, I can immediately break this whole method because we won't have anything to upload later on. So this will be bad request, actually not found. Let's call it like that. and then what I'm going to do is I'm going to check if I have existing video thumbnail key. I'm going to define UT API as new UT API and then I'm simply going to await UT await UT API dot delete files and I'm going to pass in the existing video thumbnail key And only after this is finished, I'm going to go ahead and clean up my video. So update videos, set, thumbnail key will be set to null, and thumbnail URL will be set to null as well. And then let's add our equals here. We can copy it from here. There we go. So now we have proper cleanup before we do on upload complete, right? So inside of here, we ensure that we first have to delete the existing files and reset our thumbnail key and thumbnail URL back to null. So let's go ahead and try out if that is working. I'm not going to remove everything from my database here. I mean from my upload thing database. Let me go ahead and delete this now. And I will refresh this as well. And I'm going to start and start by adding a new file like this. And I will click upload. I should now have this new file uploaded in a second. There we go. So we didn't break anything with our new middleware. And if I go inside of Upload Things Now, I should have the new thumbnail. So now I'm going to find another image. So go ahead and find some new image with a different name so it's easier to demonstrate. So confirm that you have the thumbnail in here. And now go ahead and change it to another thumbnail. So I just have another thumbnail here and let's see what's going to happen. So this should still work just fine. Great. I now have a new thumbnail. And if I refresh here, there we go. You can see the old file was deleted and I only have the new file inside of here. So our cleanup function works well. Great. But what doesn't work is my restore method here. So how about we go ahead and do that? Let's go inside of procedures here. And we're going to have to do the same thing. So we have existing video here. How about we do the following? I'm not sure if we should do that before we throw this error or not. Well, yeah, I think we can do that before we throw this error because, you know, this is a logical block when it comes to assigning the thumbnail URL from the playback ID. So I'm going to leave that in this, but I'm going to start the cleanup here. So how about we do the following? if existing video dot thumbnail key. In that case, let's go ahead and do the same thing we did in core, basically. This is what we need to do. Let's go ahead and get the UT API from upload thing server like this. Delete the files using the thumbnail URL and then update this in the database. So let's just change this. This is input.id and this is pure user ID here. There we go. So just await these two elements and then continue with further updates and assigning of the new URL. So now, obviously, this does bring one question, though. Yeah, I'm thinking, do we need to maybe upload this to upload thing as well? because we can do it using UTAPI. UTAPI has something called upload files from URL. So technically, we could just pass this and then have consistent file storage across our project. But we're going to see. I think that right now everything should still work fine. So right now, I have only one file in here. And if I go ahead and click restore, this should revert everything to my original state. And if I refresh here, no problem at all, right? No files at all That was what I was searching for Great So we have proper cleanup So I still discussing you know should I do this or not Because now we have a state where we added the thumbnail URL, but we didn't add the thumbnail key. But then at the same time, we don't really care, right? So I think I'm going to leave it like this for simplicity's sake. So we're going to know that if we are missing a thumbnail key, that means that's not something we can remove in the first place, right? So if you want to, you can decide for yourself. Do you want to use the imagemax.com or do you want to upload this URL to one unified upload service like this, right? But I think we managed to do everything we had to do, which is a factor that thumbnail fields in the schema and have proper uploading cleanup, right? But now it doesn't really make sense that we changed our database schema preview key, right? We never use this. I prepared this so that when we are inside of our webhook in the videos, when we get the thumbnail URL and the preview URL so that we can actually upload them. Maybe we should do that. I think I would be more satisfied if all of my files were in one place and not just randomly generated like this. Let's do that. I think that's a better course of action. We're going to go do that and then we're going to wrap up this chapter. So for the video's webhook here, let's go ahead and let's import UT API here like this. And then let's go inside of our video asset ready. And then we're going to rename this to temporary thumbnail URL and temporary preview URL. And then, inside of here, we're going to define UT API as new UT API. And we will be able to do await UT API, upload files from URL, like this. And that's in temporary thumbnail URL and temporary preview URL. And inside of here, we're going to have the uploaded thumbnail and uploaded review like this. So what we are going to be able to do now is, first of all, check if any of these are missing. So if we don't have uploaded thumbnail.data or if we don't have uploaded preview.data, let's return new response here. Failed to upload thumbnail or preview and return a status of 500. You know, or actually, I'm not sure if we really have to break the entire webhook just because we cannot generate a thumbnail. I don't think that should be a webhook breaking event because users will be able to upload the thumbnail anyway. Right? So, yeah. Let's see. Let's leave it like this for now. I'll see what I'm going to do with that. And now what we can do here, let me just move the duration alongside this generations here. So then here, what I'm going to do is I'm going to, from uploaded thumbnail.data, I'm going to destructure key, thumbnail key, and URL, thumbnail URL. And from here, I'm going to do uploaded preview.data. I'm going to have preview key, and I'm going to have preview URL here. And then we're going to use those. So, oh, we are already using them. We're just not using the keys, I think. So yes, let's add thumbnail key here and let's add preview key here. So now we can always store them inside of our file storage and remove them from our file storage as well. So this should now work just fine, but we also have to do the same thing for our restore method. So let's go ahead inside of our procedures for video server here. And instead of calling this thumbnail URL, let's call this temporary thumbnail URL. And then we can break out UTAPI here. We already have it. Perfect. And we can simply get the data here. UTAPI upload files from URL. temporary thumbnail URL like this. And I believe that this will just return. Let's await it, of course, first. This should return back just data. There we go. Now, I'm just not sure, should I throw an error here or not? Well, yeah, we can throw an error here. How about we call this uploaded thumbnail? Let's store this like this. In case I have no uploaded thumbnail data, I will throw new trpc error here, code internal server error like this. And let's go ahead and finally destructure the key, thumbnail key, and URL, thumbnail URL from uploaded thumbnail.data. So we should have both of these now. There we go. So now we are only using upload thing to store our files. We should never really load this on our front end in the end. We are simply using MUX as a generation tool and then uploading it. This is really good because we don't depend on MUX changing their API, right? Imagine they change it without warning you and all of a sudden all of your thumbnails have been broken, right? I think this is a much better way of doing this. Let's try all of this now. So I'm going to go ahead and do the following. I'm first going to go inside of upload thing and make sure that you have no files inside of here. I'm then going to go ahead and run my Drizzle Kit Studio here and I'm going to remove all of my videos here. So let's just go ahead and do that. So I'm going to remove all of my videos. And I will now go inside of content here. And I'm going to simply create a video. Let's select files. Let's upload a demo video here. Let's wait a few seconds. And I think we can already start refreshing here on the upload thing. And there we go. I have thumbnail.jpg and it says this is a server-side upload, basically meaning it came from our web hook. And if I click here, it is that very thumbnail, which usually we just used the pure link to image.max.com from. But now we can just use upload thing, which would also mean that we will never actually encounter this inside of Next.js image. So you could remove it. Yeah, let's actually remove it. I think we need it. I'm pretty confident we don't need it. Just make sure that you removed all videos from your database. So none of them have, my apologies, I punched the microphone. So make sure that none of your video entities have the old image.mux URL now. So I'm just going to go ahead and refresh this. And then we're just going to go ahead over our entire flow again. So I now have this here. And I also have animated.gif. Yes, we forgot about that. I just, I read it as gif. I'm so sorry. It's gif. So we also have this. and now if I go ahead inside of my content, when I hover, it works. There we go. So both the GIF and the original thumbnail are now stored in the upload thing. And if we ever want to, we can properly remove the preview, right? Because we have the preview key. Nowhere in our app are we actually ever going to remove that because we will never allow the user to upload their own preview. But if we ever want to do that, we will be able to do that because we separately stored the preview key. We don't rely on the URL at all. Great. Now let's go ahead and try doing the following. Let's change this image to my blue thumbnail here. And let's see what will happen. So the image right now should change. There we go. And let's refresh this one. There we go. the old one was deleted and only the new one is available. Now let's go ahead and try uploading another one and see if it will still work. So we have to check all of those places, right? The web hook, we have to check the image uploader and we have to check the restore in the end. So now we have a red one, which means that when I refresh here, there we go. Only this one is available. So we have proper cleanup. And one last thing to try, which is the restore method. So we are now restoring everything from scratch. Thumbnail restored. There we go. Let's refresh. And we have the original thumbnail.jpg back. Perfect. So we have very powerful cleanup. We are not wasting any storage and we are fully utilizing UploadThing in our project. Amazing, amazing job. In this chapter, our goal is to add background jobs, more specifically AI background jobs for our project. So why do we even need background jobs? There are many reasons why a project would need those But in our case we specifically want to avoid timeout from long running tasks This is specifically problematic with AI generations If you ever watched any of my previous videos where I used AI I think in two of the tutorials that I did, timeout was the issue when we actually deployed the application to production. So locally, there is no timeout. We can keep the task running for as long as we want. But when we deployed, a lot of comments came and said, hey, this is timing out and it doesn't work in production. Now, there are other solutions which are not background jobs. Those would be edge functions, which is a temporary solution because they increase the timeout level. And the other one is, well, a webhook, which would accept a successful or finished result from an AI job. That webhook procedure is more similar to what we are going to do here, which is a background job. So besides avoiding timeout from long-running tasks, another useful thing we can use background jobs for is to ensure it tries in case of a failure. And after we implement some basic background jobs, you might even get an idea of your own to where to put a background job. Perhaps our storage cleanup from upload thing, which we worked hard on in the previous chapter, could be a candidate for a background job instead of blocking our webhook, right? So I hope that after this chapter, regardless of what we do with background jobs, you will have a better idea of how to use them. And the good news is we're going to use Upstash Workflow, which is their new product for background jobs. And we already have an Upstash account because in the beginning of the tutorial, we used their rate limiting service. One thing that unfortunately is required for this AI feature is a credit card. I searched if they still offer any free tiers, but they do not. They do not offer any free tiers and they do not offer any free credits. credits. Regardless, if you create a brand new account, I tried just a moment before starting recording this and it's true. They have no free trial and no free credits. The good news is as little as $5 will be enough for this entire tutorial. And that's exactly how much I have added. So let me just go ahead and write that here. So $5 will be more than enough for you to finish this entire tutorial in regards to OpenAI queries. Now, if you have no access to credit card at all, you can still follow along this chapter, but you will not be able to properly finish these background jobs, right? But if you want to, you can learn the code and how it would work. Unfortunately, that's the only thing we can do now. If you do feel a bit more adventurous, you can find an alternative SDK. You don't have to use OpenAI. Upstash workflow allows you to do pure post requests to any API that you know of. So if you know any free SDK for AI, well, you can use that one. But for this tutorial, I'm going to be using OpenAI. It is extremely cheap. $5 is, I think, affordable for most people. And I know exactly how to use it and I know what to expect of it. So let's go ahead and start with integrating Upstash workflow right here. So I'm going to go ahead and go into my Upstash dashboard and I'm going to click on their new workflow tab right here. And if you want to, you can click on the docs. And then inside of here, you will have a nice documentation from Josh tried Upstash, or you might know him from Josh tried coding. So feel free to watch this video. That's going to give you a very good example of how it works. What we can do is we can go on to quick starts, next JS, and let's go ahead and do the following. So the prerequisites for this are an upstash qstash API keys, no JS and NPM, another package manager. So we have all of these things yet, but let's start by getting our qstash API key. Let me just see if we need that immediately. We do. So let's go ahead and get that. I'm going to go here and I'm just going to copy my qstash token. I think that's the one I need in case we need something more. We can easily come back here. And let me just say it's QStash token like this. As always, don't share your tokens with anyone. So I have added my QStash token here. And now let's go ahead and let's add bunad at opstash slash workflow. Now, since this is a relatively new product, I would recommend you to use the exact version that I'm using because it is possible that they change it fairly soon after this tutorial. You can see that it's not even in 1.0. Well, I mean, that doesn't really mean anything. I don't know how they do their versioning. But yeah, if you're interested, this is the exact version I'm using just in case you want to use the exact same one. There we go. So you would do it like this. Also, while I'm here, you might notice that I have these errors here. I believe these came from our 24-hour deletion from MUX. So let me just remind you of that. Videos get deleted after 24 hours, right? So it looks like something works incorrectly with their auto-deletion method because when we delete an asset manually, that webhook works. But it looks like when they fire their 24-hour deletion, maybe that doesn't pass the proper MUX signature to our webhook. So I think the fault is actually at their end. So yeah, in case you're looking at your app and something doesn't make sense, you have more or less videos than you would expect, it could be because your videos were deleted, right? So what I'm going to do here is I'm just going to go ahead and delete all of my assets here. Make sure you are inside of your new tube development. So I'm going to delete all of them. for a very simple reason. You know, it's a new day and I want to make sure that none of my assets get deleted in the middle of development. And I would recommend you do the same. So when I refresh here, make sure again, you're in YouTube development. You should just see empty here. And if my webhook works, this is deleted as well. In case yours wasn't deleted, you can of course go to DrizzleKit Studio and just, you know, after you've cleaned up your assets, simply go ahead and clean up your videos manually here. So I'm going to remind you this every now and then, just because I don't want you to have any confusing experience. Hey, what's going on? Why are my videos not working? All right, now let's focus back on Upstash. So we just added bun add Upstash. Let's confirm that in our package.json. I'm going to open my Git changes here. There we go. Upstash workflow is here. We have added our .environment.local here. So the Upstash workflow is powered by Qstash, which is another technology of their own. So they're wrapping their own technology. That's quite good. They can afford competitive prices to alternatives like ingest and trigger. Excellent. So now that we have that, we need to find a way to run the workflow, these background jobs. And they give us two options. One is using local QStash server, and the other one is using local tunnel. Since we already have local tunnel set up, thanks to our bun run dev all method, which runs our ngrok URL, we can use this option. Why would you want the first option? Well, the first option is useful if you want to do some heavy, heavy testing, which might affect your billing. Basically, the logic behind this is that local QStash server will not affect the billing. billing. Whereas if you massively test via your local tunnel, that will literally call the AppStash, QStash and workflow. So it might affect your usage. But as you can see, I'm on a complete free plan. I don't even have my credit card added here. So I'm pretty confident that for this tutorial, we're not going to overstep any limits at all. Great. So we're going to choose option two, local tunnel. So copy the Qstash token from Upstash console. We already did that. We can confirm that by going inside of .environment.local here. Qstash token is here. And now we need to add Upstash workflow URL. Now the Upstash workflow URL is going to be our static ngrok domain. So let's go ahead and get it here. There we go. Like this. I'm just going to go ahead and add an HTTPS prefix like this. After that, let's go ahead and see what else we have to do. So this looks like it's working. We already have the QStash token. And now let's go ahead and let's create a workflow endpoint. So this is quite simple. All we have to do is we have to create a route somewhere in our app. So I'm going to go into the source app folder API. Let's go inside of videos here and let's create workflows like this. And then let's, for example, create title and then route.ds. So inside of videos, we're going to have workflows and then title and then route.ds. So this is the equivalent to localhost 3000 slash API videos workflows slash title. Basically, a workflow for title inside of the videos API. And let's just copy this entire thing from here. So they are using a different example, but ours makes more sense like this. So import serve from upstash workflow next JS. And inside of this serve, they have prepared two steps for us to run, and they have also overridden the post method. I'm not sure if overridden is the correct term to use. Maybe they've created a post method for us thanks to this serve method here. And this is actually it. This is the background job, right? So here's what's important now to test this out. The first important thing is that you have your webhook running, right you should be able to visit this url right here if you cannot visit this url it means your webcook\nis not running and your testing of this will not work. The other thing that you have to do is you have to try posting this endpoint, which we just created. And once you post this endpoint, this code inside of these steps will not run on your backend, but instead it will be a background job in Upstash, which has much higher timeout limits. It has proper retries in case of error. And you're going to see exactly what happens with each step inside of the usage here, inside of events. And in case something fails after it tries, you have this dead queries, which then you can manually check out like what's going on, why did they fail? And then you can debug and you can refresh them if needed. Let's go ahead and let's try doing this. So I believe that this is what they say to do next, npm run dev, which is our bun run dev all. And now all we have to do is we have to do a curl and post method to our API workflow. So what I'm going to do is I'm going to change this to go to slash videos, workflows, slash title. And let's see what's going on. It looks like I am getting an error. Of course, I'm getting an error. I think that we have to do it on, or maybe we don't have to do it on Google. Maybe it is an actual issue here. Let's see what exactly is going on here. The issue is that I used HTTPS. There we go. So don't use HTTPS. My apologies. I thought that the issue was because I have to run the local tunnel, but that's not true. We should be able to query on our local host, and then the local tunnel will be used to communicate from AppStash back to our application. So if you want to, if you have the ability to use the curl command, don't worry, we're later going to test this using the usual interface. But if you want to, you can try this, make sure it's HTTP, localhost 3000, API, videos, workflows, slash title, and you should get back to workflow run ID. And now, if you go back inside of here, there we go. You have the job and you have all the steps which were happening here, the initial step and the second step right here. Perfect. So you can use these steps to do whatever you want, but just these steps just by themselves don't seem too useful, right? The reason they don't seem too useful is because we don't really know what's inside of this context. There's no way for us to run this in any other way than a curl post. So what we have to understand next is how to actually run these kinds of apps. So they do have a step five deploying to production. We're going to focus on this later when we actually deploy. So there are a couple of things we have to do now. First of all, I believe we have to secure our endpoint. Basically, we need to have the Qstash current sign-in key and the Qstash next sign-in key. And there are two ways they allow securing. You can either use Qstash's built-in request verification, or you can develop custom header and custom authorization mechanism in case you need that solution. So let's go ahead and go back to Qstash. And let's go ahead and click on signing keys. And let's prepare Qstash current signing key. So I'm going to open my .environment.local here. So Q-stash current signing key and Q-stash next signing key. And let's go ahead and copy the first one, add it here, and the second one, and add it here as well. Both of them begin with SIG, pretty sure, short for signing or signature. Now that we have those, let's just confirm that we properly added them. So Qstash current signing key and Qstash next signing key. The names are important here because I believe that the Qstash, we don't have Qstash yet, but I believe that when we actually create a lib for Qstash, it will automatically look for these environment variables here. And that's how it's going to secure the endpoint. So now that we have added those, if we try this again, I'm not sure. I think maybe it will still work. It looks like this doesn't work now. There we go. Because it detected that we are using Qstash current signing key and Qstash next signing key inside of our application. But this request, which we just tried, doesn't have those. So it's no longer allowing anyone to just trigger a workflow from our application, right? Only we who have these keys can query our endpoint and trigger a background job. So this is now secure from any intrusions or any attacks which are aiming on overflowing our background jobs and causing massive usage or billing. There we go. So we successfully secured our QStash background jobs. Now the question is, how do we call this? And as you can see, you can have this. They do support the edge cases in case the environment variables are not supported. Then you can explicitly protect it via receiver like this. But we don't have to do this step. As you can see, we don't have to add anything here because we support environment variables normally. And of course, they also offer the custom authorization method. if you are in an environment where you need that. So now that we know how to secure our run, let's learn how to start a run. And this is the recommended way of doing it, which is client.trigger. So we have AppStash workflow. So how about we go ahead and define this? I'm gonna go ahead and go inside of lib here. So we already have the Redis and we have the rate limit. And now I'm gonna create Qstash here. You can also name it workflow if you want to, but I think Qstash may be a bit more specific. Actually, let's call it workflow. Let's go ahead and let's import the client from Upstash workflow. And let's go ahead and export it from here. And I'm just going to call this workflow. And inside of the token here, we need to pass process.environment. and we need to pass the Qstash token, which was the first thing that we added. And I will put a little exclamation point at the end. There we go. So we now have the workflow. I'm still deciding on whether I should name this constant workflow because I'm fearing that we might have conflicting names between the imports, right? Something else might be named the workflow. So that's why I wasn't sure whether I should name this Qstash or workflow. But let's leave it like this for now. And this is how you would run it now. So let's go ahead and let's try this now. We're going to go ahead and do the following. We're going to go and create a little procedure, right? So we immediately connect this to our code. Let's go inside of videos. Let's go inside of server and procedures. Inside of here, we already have the restore thumbnail. How about we add generate thumbnail? Let's add a protected procedure here. The input can be empty. And let's add a mutation here. Let's actually remove the input in case it's going to actually be empty. Let's make sure this is asynchronous. And I'm not going to do anything here besides just triggering a call. So I'm going to go ahead and do await workflow from lib workflow. and I will simply call trigger like this. And inside of here, what I have to do is I have to pass in the URL, which actually I'm not sure if we need to pass this explicitly. I think that if we've added upstash workflow URL, I think that it's actually going to read that URL. So let's try without passing it. And then we're going to see maybe URL is a required parameter. Maybe I am wrong. Let's just do workflow run ID here. So this is, oh, my apologies. I got confused about how this works. We need the URL. Let's add process.environment and let's add upstash workflow URL. And the way we choose which workflow we are triggering is by going to slash API slash videos slash workflows slash title. So this is how we choose which workflow to trigger. Everything else is optional, but if you want to, you can pass a body. I am a body. Or maybe more interesting, it would be if we could destructure the context from here. If we could get the user ID from context.user like this, and then we can pass in inside of the body here, user ID like this. So we know which user triggered this background job. And we would have the workflow run ID here, and we can just return that back. So now we have the generate thumbnail protected mutation. So let's go inside of our form section. And we have this drop down menu item AI generated. So how about we copy the restore thumbnail here. Change this to generate thumbnail. Use videos, trpc, generate thumbnail. It really doesn't matter what we do here. But yeah, we will eventually, actually, since this will only trigger a background job, we should not do any of those, right? Instead, what we are going to do here is the following. We can add a title, background job started. And description, does it allow title? let's see, background job started, and then maybe description. It could be, this may take some time, like this. And now let's use the generate thumbnail, similarly to how we use restore thumbnail restore that restore thumbnail here So on click here generate thumbnail mutate and we don pass anything inside for now So now what should happen is once I click on this let me just go inside and create a random video here. I forgot that we removed all of our videos. So let me just go ahead and upload this demo again and just wait a few seconds for everything that needs to be generated around it get generated. So I'm just going to refresh here. It's loading. It's loading. Looks like it's still preparing the video. Hopefully soon. There we go. Ready and ready, which means we have the thumbnail. Great. So now if I click AI generated, this should fire a new background job. There we go. background job started, this may take some time. And now if I go inside of Qstash, instead of workflow, I should see API workflows title right here a few seconds ago. And you can see it successfully passed the user ID here. So now this background job all of a sudden in my workflows title has more sense all of a sudden, because now we know that this context has the user ID, which triggered this API call. That's something we did not know until now, right? So let's go ahead and learn how to actually extract the body from this context right here. What I like to do is I like to create an interface input type here, and I'm going to pass in the user ID string, and how about we pass in a video ID string as well. And then what I'm going to do is I'm going to go ahead and define input to be context request payload as input type, like this. And then in here, I will be able to destructure my video ID and my user ID from the input. And then what we will be able to do is, for example, we would be able to dedicate one of these runs to do a long running task. For example, this is not exactly a long running task, but what we could do is find a video. So this would be existing video. And we would do context.run. And we can call this get video like this. And inside of here, we would get the data, which will be await database dot select. Let's just import the database from add slash database here. I will just reverse these two imports. So await database, select everything from videos from our database schema here. And let's go ahead and add where. Let's add and, and let's add two equals. And inside of here, we're first going to check if videos ID match our video ID. And the second one, if user ID matches our videos user ID. Actually, we should reverse this too. There we go. And then let's go ahead and return data zero. And inside of this step, if we want to, we can check if that exists or not. So if data, if the first element in the array does not exist, we can throw new error here, not found. And this will break this step. And then when we have the existing video here, we can continue using it. We can console log the existing video if we want to. Or, you know, we can continue doing something in step two. And the cool thing about these steps is it doesn't make too much sense, you know, to just do a normal database fetch inside of a background job, right? That's not a long running task. But you can already get the idea of what this could be useful for. And we can separate all of these things in these steps. What I particularly think this is useful for are webhooks, especially our videos webhook. Because we do so many things in each of these things. We do thumbnail generation. Then we do thumbnail cleanup. Then we go ahead and update the video, right? Those are three steps which we could delegate to a background job if we wanted proper retries and order and everything, right? And especially for things like this, right? What if the thumbnail generation fails? Our entire webhook fails in that case. So that's something that we can dedicate to a background job. And then if it fails, it's simply going to repeat this step and only then it's going to go to the next step. So how about we try this again? I'm going to go ahead and prepare my runs here. At the moment, I only have two runs. So I'm going to go ahead and try this again. I think that I first have to modify the input here. I will just copy the one from restore thumbnail. So let's also accept the ID. Let's go back inside of the form section and we will just pass this here. There we go. So let's try clicking AI generated. And now if I refresh here in a few moments, we should be seeing the new background job. Let me try a hard refresh. Let's see, maybe we've gotten an error here or something. I don't believe it's that. Let me see. Oh, it looks like we do have an error. Error submitting steps. QStash in parallel step execution not found. Okay, let's see what's going on. Maybe it was because the moment I saved and run the query, I didn't refresh. So it never got the proper. Yes. So the issue is that it never got the video ID because I clicked save and I didn't wait for hot reload to update, I believe. Or maybe my procedure doesn't even send it. Yes, let's go ahead and let's extract video ID and use input.id here. Input from here. Like this. There we go. And this is an accidental import. Yes, so this one, for example, keeps failing, right? This get video step, for example, is now constantly failing. And if it fails more than three times, which I think is the default, maybe not, maybe I'm incorrect. Nevertheless, I could define my retries here. So I could say, hey, only try it three times. Don't try more than that, right? And once that happens, once it fails, it will end up here in, I'm not sure what this is short for, maybe deadline query. I'm not sure. but it is descriptive and it will allow you to have a separate view of all of the steps or jobs which failed so that you can manually go here, you know, take a look at why they failed, see the body that they had, you know, what's going on and then try and manually rerun them. And if you have a task which you know is incorrect like this one, you can always close it yourself. So this will cancel a workflow. So now that we are properly passing in the video ID from our input.id, let's go ahead and refresh this entire page. So we ensure we have three of those. And let's try running another one. So this is some unrelated issue, maybe some hot reload. Yes, I think this is something hot reload related. What we're going to do here is we're going to try this again, AI generated. Background jobs started. There we go. This one was canceled. Exactly what we expected. And in a few seconds, we should be getting a new one. But it looks like we do have some error here. So let's go ahead and debug this one together. This is an upstage workflow error thrown after a step executed. It is expected to be raised. Make sure that you wait for each step. Perhaps I did something wrong again here. It's specifically here with existing videos. So maybe I'm doing something wrong and I didn't even notice it here, which is funny because all I'm doing is trying to demonstrate the video fetching here. Let's see, videos ID, video ID, and videos user ID from user ID. I think this should work fine. And it looks like it does work fine. there we go. So get video right here. And you can see that it got the entire video. So I'm not exactly sure what this error was here. Maybe this video actually came from something from before. Right. Maybe that's what's going on here. Because this one looks like 200. Yes. I think this came from something else. Great. So I think that we've now kind of established what the background jobs will be used for. We've secured them and we learned how to trigger exactly the job we need to trigger. So now let's go ahead and let's, well, let's make this actually does what it needs to do. Let's go ahead and use the generate thumbnail one, which I know sounds funny now, but we've already called it generate thumbnail. Let's still make it use Workflows title and let's use it to update the title of a video. How about we do that? Right. So we're going to break the step here. Let's go ahead. I'm going to call this video. I'm going to head in the existing video here. If we can't find the existing video, throw an error and otherwise return the existing video. That's going to be my first step, Get video. Now in my second step here, what I want to do would be update video. So I'm going to go ahead and simply do await database. Select my apologies update videos. Set title updated from background job. Where and. and let's just copy these two here. And make sure this is asynchronous And we don need the last step right So we now have the first step which gets our video So we could use yeah I mean we don really need this It's more of a, for demonstrative purposes. So we could, you know, just do this one. If you want to, you can still use this one. And then maybe from here, I don't know, you can use video.id from above and video.us. user ID from here. I just have to await the context run to get the video user ID. An example, right? So you don't no longer rely on the props here, but you now rely on what you actually fetched. So let's go ahead and try this now. So now after I call my new background job, this title should change for this video. So I'm going to click AI generated. This started a background job. And after that background job successfully runs, it should change the title of my video. There we go. This is my latest run here. And we can see that we successfully got the video and it successfully updated the video. So let's go ahead and refresh this page now. There we go. updated from background job. And now I'm pretty sure you understand all the steps we need in order to use AI to do this step for us. So let's go ahead and let's go to OpenAI Platform. So make sure to go to the platform and not to their chat instance and go ahead and log in. So I've created a brand new account for this one just to confirm that, unfortunately, there are no free tiers. You will have to add something. So I've added $5 here. And now what you have to do is you have to find your API keys. Let me go inside of API reference. I'm not sure how to use this interface, to be honest. Let's go into settings, API keys, and let's create a new secret key. I'm going to call this next new two and my default project. And I will select all for permissions. And I will copy this. And now let's go ahead and let's store it inside of our environment. So open AI API key and let's pass it here. There we go. So now what we have to do is we have to go inside of, my apologies, inside of the documentation here, and you can find integrations with OpenAI. And inside of here, you can see how you can make a context API call specifically to OpenAI. So there are two ways you can do it. You can use their SDK, Context API OpenAI Call, or if you want to, you can. Let's go ahead and see. I'm not sure where. Maybe this is the example. Let's see. You can just do a normal HTTP request, but I can't find an example now. But they also support Anthropic. They support Resend, Vercel, AI, SDK. But this is actually quite interesting. I don't think this was in the documentation when I was building this. They've added an example to use DeepSeek. This is very interesting because maybe you can do it for free. If you really have a problem with adding a credit card, you can go to apideepseek.com and try creating a free account. I'm not sure if they have any limits. I have no idea how their API works, so I can't guarantee you that it's free. but you could try using that instead. And you can still use OpenAI call here, it seems, because they support the exact same API. Great, but since I've already decided on OpenAI, I'm going to continue with this. So I'm going to go ahead and copy this. I'm going to go inside of my route title here. And inside of my, I'm going to add another step here, which I'm going to define const generated title. and this will be await, and then this. Basically, I'm going to await context API open AI call, and I'm going to call this generate title. So this is the same as context.run, the name of the step, and then just the configuration. So let's go ahead and change this to process.environment, and let's go ahead and let's use open AI API key. Add an exclamation point at the end. Operation is chat completions.create. Let's see, is this strictly typed? It is. So you should be able to see everything they have here. There we go. Chat completions.create. Models. Looks like models are not strictly typed. So you have to be careful here. Make sure you type it like this. And then inside of here, you can give a role to the system and then what the user asks. So let's, for example, try and do that. So the role for the system here should be a prompt. It should be a prompt which would basically tell the OpenAI what they have to do. And if you have access to my source code, you can simply find the title system prompt. Otherwise, you can just use the public gist where I've added these logos and you can find the title system prompt, for example. And let's add it here. So title system prompt. Your task is to generate ACO CEO focus title for a YouTube video based on its transcript. Please follow these guidelines right here. So just a simple system prompt here. And then that's what we're going to add to the system. And then the user can say, hi, everyone. We are basically mocking a transcript of a video. Hi, everyone. In this tutorial, we will be building a YouTube clone. Let's try if this works, right? This is not a real transcript, so I'm not sure how well this is going to work, but we can try. And what this can do, this OpenAI call can do now, you can immediately destructure the body from here like this. And then you can update the video based on the results. So for example, let's get the title here to be body choices, first one, and then this will be optional. So message, optional, actually content looks like it always exists. There we go. And then let's pass that as the new title here. The issue is that this can be null. So let's default this. Do we have our first, we have the video here. Great. This is actually useful. Now, let's just fall back to our original title in case this generated one is null for whatever reason. So if you've added your OpenAI key, this should now work. Let's try and see. So right now it says updated from background job. But if I go ahead and click AI generated, and if I go ahead and just focus on this, wait a few seconds, we should see a new title now. There we go. So finished a few seconds ago and looks like it successfully generated the title in this step, as you can see. So we can see the response. I'm not sure how good it will be. I'm not sure where can I read the message here. Let's actually see what did it do. Did it do something useful? There we go. Build a YouTube clone step-by-step tutorial. It's actually amazing that it generated such a good prompt just from this very simple transcription. Great. So that is basically what we wanted to achieve with these background jobs. I hope that now they make more sense, right? Because this kind of thing is a potential long running task. I think even this one is quite simple, so it could be awaited. But imagine you're using this to generate a thumbnail. Imagine you're using this to generate parts of the video, right? Any long-running task that you can imagine is better off put inside of a background job like this one. Excellent. So now that we have this, let's go ahead and let's actually get our transcript, right? And the good news is we have the transcript. You just don't know how to get it. So in order to get the transcript, we first have to get our video. After we get our video, let's create the transcript. And let's do that in a background job as well. So context run, get transcript, asynchronous. And we're going to get the track URL from HTTPS stream.mux.com. video from above max playback id slash text and then we would need max track id video max track id dot txt so this is the important thing right max track id gets generated inside of our videos webhook when we get video asset track ready. So only then we generate the track ID and we add it here. And remember, this webhook only fires if we enabled the subtitles, which we do inside of our video procedures here. So we added this part. So that's the first requirement for that webhook to The second requirement is that there is actual audio, and I think in this case, English audio in the video. And only then will you get this part right here. So in order to correctly test this, I would highly recommend using a video with English audio. If you remember in my previous chapters, I'm not sure which one it is. Is it this one There we go You can visit tinyurl slash new where I gave you a demo MP4 which I using for my jobs here So find that video or any other video and confirm, you can confirm inside of your studio that you have a MUX track ID. So basically, I want you to confirm these things so you don't get confused as to why my works and your doesn't, right? So be careful and confirm that whatever video you're testing this on has MUX track ID. And you should also have MUX track status set to ready. So if subtitles are ready, you should be able to do what I'm doing here, which is generate a text file from stream.mux.com. Now we have to actually fetch this. So let's do await fetch track URL. And let's return response. This is a typo. .text. There we go. So we now have this right here. So we can call this final transcript. Or let's just call it text like this. If there is no text, throw new error, bad request. Otherwise, return back the text, which will ultimately be the transcript. Great. And now inside of here, you can pass the transcript like this. There we go. And inside of, you can move the title here, I believe, and you can do the same thing here. if for whatever reason the title was not able to be generated, you can throw new error here and simply say bad request. There we go. Actually, it might be better to do this outside of the step because throwing an error here will cause it to retry, but it couldn't do anything new with the result it got. So perhaps throwing it here would be a better idea, right? I'm not exactly sure how this one gets retried. But I think since they've built, they have this built in, if you return the body, I think that it will, if it errors, I think that it will just retry in itself, right? So first thing that I want to confirm is that something like this works. So let's go ahead and just go inside of mux here. And I want to go inside of docs, just to show you exactly where I found this URL. So it's not just magic for you. Let's go ahead. I think they actually have a section called AI workflows, automatic translation, summarizing and tagging. So go here. And then inside of here, they should teach you how to retrieve the transcript file. So if you click here, we've added this. And now we should be able to retrieve the file. There we go. So this is how you retrieve it. Stream.mux.com, playback ID, text, and then track ID. Of course, if it is a signed asset, it will also require a token. That's not the case for us. So exactly what we did here in case you're wondering, where did I get that from? Perfect. Let's try it out. So I know I have some audio here, but honestly, I don't even know what I talk about in the video. But let's see what AI will generate. I know it's funny we use the thumbnail button, but that's because it's the only AI button we have, right? So let's go ahead and wait a few seconds for this new job to finish and see if our title gets updated. There we go. Looks like it was a success. and you can see how get transcript works. This is what I was interested about. This is a short clip with English audio so we can demonstrate max upload and subtitle generation. Thank you. So this is exactly what I say in the video, I believe. And let's see what's my new title now. Max upload and subtitle demo. I'm so impressed by this. This is so cool. The way we use transcript, a background job, all of that combined to generate a title. I think that's super cool. So what we have to do now is we have to add dedicated buttons here for AI, here for AI, and then generated AI thumbnail generation. So let's go ahead and do the title one. So let's start by going back inside of the procedures here and inside of the videos procedures. And let's find the generated thumbnail one. and let's just copy and paste it above and call this one generate title like this. And it's going to go to API works for title. Nothing else should be changed really. And now let's go back inside of our form section component. Let's go ahead and copy this generated thumbnail and let's rename it to generate title. And everything here can stay the same. And now we have to use this generate title somewhere. So let's go ahead and find our first input, which is the title. There we go. To do add AI generate button. So I'm going to go ahead and add a div here, remove the to do, and give this a class name of flex item center and gap X of two. And then below that, I'm going to add a button with sparkless icon. And in here, I'm going to go ahead and give this a size icon, a variant of outline, a type of button, very important, class name, rounded pool, size six, and then I'm going to lower the size of the SVG using this targeter. and on click here, I'm going to call generate title, mutate, and I'm going to pass the ID, video ID. And disabled if I have, it's going to be disabled in case generate title is pending. And then we can also create a nice little if generate title is pending. In that case, we're going to render a loader2 icon with the class name of animateSpin. Otherwise, we can render the sparkles icon. There we go. So now we have a nice AI button here. So if I click here, it says background job started. This may take some time. I'm not sure how different the new title will be because this one is already relevant to our transcription. So now that we have this, let's go ahead and copy the entire content inside of this form label here, including the title text here. And let's find the description and let's just do the same thing. And I will just change this to description. like this. So now the description will have it as well. Let me refresh to see. Maybe we got a new video. We did a max upload and subtitle generation demo. So it created a new one regardless. Great. So we have this. Now let's go ahead and go inside of procedures and let's copy and paste generate title. This one, let's make it generate description. And let's make it go to API videos, Workflows Description. Description, like this. And now we have to create the description workflow. But that's going to be quite easy because it is exactly the same. So inside of Videos, Workflows, let's change this to Description. And let's change this. Well, you have to find the prompt from my assets here. So Description System Prompt. You can write your own if you want to. There we go. And simply use the description system prompt here. And this will be generate description. And then call this description. And simply update description. Like this. So that's our new background job with the new description system prompt. And now we have to go inside of our form section. Our description button currently uses generate title. Let's change that by copying and pasting generate title and renaming this to generate description. There we go. And now let's go ahead and let's replace this. So this is for the title. That's correct. But for my description, these three should be generate description. There we go. Let's go ahead and try it out. So I'm going to click this now. Background job started. Perfect. And now I'm just going to wait and see if my new job for description will work as well. So this should show slash description in a few seconds here. There we go. Run success on slash description here. And let's go ahead and try it out. And so if I refresh this, this video demonstrates how to upload the video to MUX and generate subtitles. Amazing. Exactly what we expected. So the only thing that's left here is AI generated thumbnails, but I will leave that to another chapter simply because it is a whole new component that we need and a prompt here that we need. And this is already an hour long, but I think you got the gist of how we're doing this. We will of course finish this, but just in the next chapter. And one thing that I would highly recommend doing is also disabling these buttons if video itself does not have MaxTrackID. Like this. So if it doesn't have MaxTrackID, you can completely, you know, prevent it from even trying to generate something. the only videos which are not going to have max track id are videos whose subtitle status is not ready for example that could be a video that you will just upload right so let's go ahead and select this demo.mp4 here and right now you can see that you can see that you can see that you can see that\nSee, it's disabled and we want it to be disabled because it has no subtitles yet. If I refresh now, there we go. Since subtitles are ready, I can already do that. It looks like I already have my track ID, which is interesting because I always expected the video status to be ready before the subtitles. That is interesting. The reason I say it's interesting because in my videos webhook in the track ready, I depend on the asset ID. But I only assign the asset ID in my, let's see, when do I do it? So in video asset created. Okay, so let's hope that video asset created fires first. first. Because if this, if video asset track ready fires before my video asset created, I never actually have the asset ID to look for in the video. But looks like it's working, right? So if I go ahead and try both of this, yeah, I can just start both of them, right? That's no issue. So if I run both of this now, I should be able, it should work. But you saw how in the beginning, Both of them were disabled because we did not have any. There we go. So title was generated. And I believe the description will be generated in a few moments as well. We can, of course, keep track here. There we go. This is the one. Looks like it's still running. It's generating the description. Let's refresh. Maybe it's done. Looks like it's still running. all right i'm going to give it some time to run maybe there is an issue maybe we're doing something incorrectly here but i think everything is correct it has both the video id and the user id so let's refresh looks like it's taking some time i'm going to pause the video and unpause when it finishes. So it's been two minutes and it's still running. So I am going to debug why this is happening. Perhaps for you, it works perfectly. But, you know, looking at my logs here, only title has been hit so far. It could be that I did something wrong when I fired them simultaneously. So I'm going to check out the documentation on that part, but I will also leave running. Perhaps the fact that we are on free tier and we run two queries at the same time caused this one to be queued for after some time. So I'm going to go ahead and end the chapter here and let's just see if we did everything we were supposed to do. So we integrated Upstash workflow. We triggered a background job. We've set up OpenAI SDK. We've generated the title and the description, but we will leave the thumbnail for the next chapter. Great, great job. In this chapter, we're going to go ahead and wrap up our form section by implementing AI thumbnails. Basically, this little button left in our thumbnail dropdown. So the goal is to create a thumbnail prompt model so that we can go ahead and enter what we want for AI to generate because it doesn't really make sense to use the transcription for image generation as that really doesn't work well in my experience. We also have to create the proper generation workflow and wrap up form section by adding the skeleton to its loading state. And just some update about my last run. Looks like it ran for five minutes. I'm not sure why it took that long, but the good news is it works. It definitely worked. It took its time, but it absolutely did what it was supposed to do. And that's one cool thing about background jobs is that they are very reliable, right? So even if they don't seem to be working right away, you can know for sure that they will at least retry if they fail. And you will, of course, have all the logs and analytics you need to discover why some things happened or didn't happen. And here's the new description. So let's go ahead and create the thumbnail upload, the thumbnail generate model. We're going to go ahead inside of our modules, Studio, UI components, and let's copy the thumbnail upload model. Let's rename it to thumbnail generate model. Let's go ahead and change this to generate. We are going to have the video ID open and open change. All of that is fine. And we're also going to have form schema here. using Zod. So import Zod. And we're just going to add prompt. And let's go ahead and make it have a minimum of 10 characters so that it's at least something descriptive to tell the AI to do, right? I'm just going to move this to the top. And what I'm going to do now is I'm going to go ahead and change this to not use an upload drop zone. Instead, we're going to go ahead and open our form from components UI form. So while we are here, let's go ahead and end everything else we need from our form here. So that's form control, form field, form item, form label, and form message. We can remove the upload drop zone from here. And let's also add text area. That's going to be our component. And we're also going to need use form and Zod resolver. And let's wrap up our imports by also adding host from Sonner. One more component is the button so we can submit the things. There we go. So we have the form schema and we have prepared the form here. Looks like it says cannot find form. Oh, I forgot that I removed a form. Okay. Now let's go ahead and let's create the form constant here. So use form z.infer type of form schema, resolver, zod resolver, form schema and default values prompt empty string like this. There we go. So now inside of here, we can go ahead and spread the form. And inside of here, we can open up a native HTML form element. And we can pass in on submit to be form handle submit, pass in on submit. and we can turn this into an on submit method here. So we can leave it like this for now. We are going to modify what's inside in a moment. And let's also give this native HTML form a class name of flex, flex column and gap four. Now let's add the form field, which is a single self-closing tag here. Let's give it a form control. Let's give it a name of prompt, which is the only possible option. And let's go ahead and give it a render. Inside of here, a form item, a form label saying prompt, a form control, and text area. Let's give this everything necessary from the field. Let's give it the class name of resize none, columns 30, rows 5. And let's go ahead and give it a placeholder. So for example, we can write anything we want here. You know, a descriptive, a description of wanted thumbnail like this. and let's go ahead and pass in form message here as well. And still inside of the form element here, add a div with a class name, flex justify end, and render the button, which will say generate, and give it a type of submit. So before we move forward, let's go ahead and render the thumbnail generate model component inside of form section here So I going to render it right next to our thumbnail upload model So thumbnail generate model and let import this from thumbnail generate model Let's copy this state and this will be thumbnail generate model open and thumbnail generate model open. Set thumbnail generate model open. Great. So now in here where I render it, I'm going to modify the thumbnail generate model to use thumbnail generate model open and set thumbnail generate model open. So just be careful. Make sure you don't mix and match invalid states here, right? Video ID can stay the same. And now we have to reuse the set thumbnail generate model inside of our sparkles icon AI-generated drop-down menu here. So instead of this, what's going to happen? It's going to set this to true. There we go. Let's see what kind of error I have now. So I have generate thumbnail here. We can remove this. We are not going to need it here, but we will need it inside of the thumbnail generate model. So you can add it here if you want to generate thumbnail. And let's see, do I have TRPC? We have it from the client. So add it here, generate thumbnail, TRPC videos, generate thumbnail. There we go. And on submit, we're going to call the generate thumbnail. That's fine. Let me just remove. Oh, did I? Okay, it should be here and not in the form section. Yes. Okay, now I believe if I go ahead and click AI generated, it will open upload a thumbnail. Perfect. So let's go ahead and just finish up our on submit method to accept the values of z.infer type of form schema. And what we're going to do here is simply call generate thumbnail, mutate and pass in the prompt to be values prompt. and ID, video ID. Now we are getting some errors here because we have to modify our generate thumbnail procedure. And what we're also going to do, well, we don't have to do anything. We don't need to use the utils at all, but we just have to fix this prompt thing. So let's go ahead and go inside of the TRPC videos generate thumbnail protected procedure. So whenever you find this, you can press command or control and click here, and it should lead you to that exact procedure. It's located inside of videos, servers, procedures. So besides ID, we are also going to accept a prompt, which will be a Z string with a minimum of 10. So it's the same thing that we set here. Great. We now have this. So besides user ID and video ID, let's also pass the prompt from input dot prompt. I'm not sure why it didn't. When I wrote it like this, it looked like prompt exists. It looks like prompt is a reserved function. So that's why there was no error. So be careful. Yeah, you have to destructure the prompt from the input. I did not know this. So maybe prompt isn't the best name to use, but make sure that you pass input dot prompt. So when hover over here, it should be string and not that function. Well, yes, prompt is a pretty popular method, but I have not used it in ages. Great. So we now have generate thumbnail and let's change the workflow to go to thumbnail because that's the workflow we care about. And now it's up to us to create that. So let's go inside of source, app folder, API, videos, workflows, and let's copy one. For example, let's copy title and paste it here and rename this to thumbnail. Let's go inside of the thumbnail one. And now it's time to build this route. So first things first, besides having this, we're also going to have a prompt. So let's add that. We can remove this default prompt because the user will type their own. And here we can also destructure the prompt now. And let's start as we usually start, which is by getting the video. So in case we can't get the video, we're going to stop here. We're not going to run any other steps, right? Now we are not going to need any transcript. At this point, we can already generate, but we're going to generate a little bit differently. So OpenAI SDK from Upstash, at least in my version, does not have it for anything other than chat completions create. So what we're going to do is the following. Let's go ahead and delete this. And let's go ahead and do context.call, generate thumbnail, sorry for the pop-up, generate thumbnail like this, and then pass in the second argument. And in here, we can go ahead and pass in the API openai.com version 1 images generation. So I cannot exactly guarantee you that in the future, if you're watching this a year or two from now, this endpoint still works and still uses the same API. So if you want to, you can go to OpenAI documentation. Let me just load the platform here. and if I click on image generation, images API, there we go. API openai.com, we want images generation. So find in the OpenAI platform images and in here, you should be able to see if this still exists or not. So you can copy it from here and also add it right here. There we go. And then you can follow the prompt, the model, right? Let's go ahead and pass in the body from here. So let's assign the method to be post. Let's assign the body to have our prompt like this. And then you can go ahead and pass in some other things. So n will be one. I'm not sure what this actually means. The number of images to generate. Okay. So we are going to default to one. And now for the size, let's go ahead and see. So it looks like we can modify the size. In my original source code, I was using this one, but this is not the greatest resolution to use for thumbnails. So perhaps if we change the model, let's see. It looks like if we use DAL E2, it's only square images. But if we use DAL E3, we can use this type. So let's try that. How about we give this a model, and now let's give it a size. We're going to try both of this. So how about we try this one first, like this. And now we also have to pass in the headers here, authorization. Make sure you don't misspell this because this is not strictly typed. Pass in the bearer and then process environment. And you have to add from your dot environment, open AI API key like this. There we go. Perfect. So after this, if you go ahead and go, you will have the body here. And what you can do is you can get the temporary image, temporary thumbnail. URL like this, and you can extract it from body data dot URL. At least this is how it works here. So body data, and then first in the array dot URL, right? So I'm not sure how we can set, can I go ahead and give this a type of body, which is going to be this, URL string, and then an array? Will that work? Let's see. Did I do this correctly? Body. Okay so maybe just this Maybe just URL inside My apologies How about we do... Oh, like this? Body data doesn't exist on type. Let me try and strictly type this. Oh, yeah. My apologies. So we just need data and data needs to be an array of this, right? There we go. So if you want to, you can add your own types for what the return is going to be. And if you don't like this return, this array, you can also do this. Yeah, that's the same thing. Great. So we now have this temporary thumbnail URL. In case we don't have it, we can immediately throw this as bad request. And now what we have to do is upload it to upload thing, right? So we are going to do the same thing that we did in our, is it a web hook? In here, when we do asset ready, we upload from the temporary thumbnail URL. So we're going to do the same thing here. The reason we did it in here is so we don't have to rely on MUX changing their API in the future and then all of our images get lost, right? And to own our images. But the reason we are doing it here is because this URL, which OpenAI generates, will only last for some time. It won't last forever. They are not a storage company, right? So we shouldn't expect them to store these URLs. You should always treat them as temporary thumbnail URLs. So now let's go ahead and do the following. We're now going to create uploaded thumbnail URL. And let's do await context run upload image. Upload thumbnail. Let's do that. Asynchronous like this. And let's define UT API to be new. UT API from upload thing slash server like this. And then let's go ahead and destructure data from await utapi.upload files from URL and pass in the temporary thumbnail URL. And just return back data. There we go. And in case we have an error, we can just throw new error. Can I pass that here? I can't. error dot message. Maybe we can do that. There we go. Or simply if we don't have data, we can go ahead and say bad request. There we go. So now this will be the uploaded file data. So maybe we should just call it uploaded thumbnail. And then what we have to do is, let's see. So in here, we confirm the existing video. So we have that here. What we are supposed to do now is we are supposed to do the cleanup. So let's do the cleanup now. Await context run. And this will be delete or let's call it thumbnail cleanup or cleanup thumbnail. So we stay consistent with our names here. Asynchronous. and what we're going to do here is define another UT API. Perhaps we can just move this here in the beginning because we're going to use it throughout so we don't have to define it many times. So what we're going to do is we're going to check if video has thumbnail key. In that case, await UT API delete files by video thumbnail key. That's what we're going to do. and then await database update videos set thumbnail key to null and thumbnail URL to null like this. Where and and pass in our two equals here. We can just copy this. There we go. And after that, this should be fine. And one thing I actually want to do, I want to move this step before we upload the thumbnail. That's what I want to do. So I first want to clean up our old ones just in case, you know, this never succeeds. And then we're left or, you know, this succeeds, we upload. Basically, I'm failing to explain what I mean. I first want to do the cleanup, right? Before I upload anything new, I want to delete the old. That's it. That's what I wanted to say. So only then are we going to go ahead and upload new things here. And finally, update video. This can stay the same. And this time we're changing the thumbnail key to be uploaded thumbnail.key and thumbnail URL uploaded thumbnail.url. There we go. That's the workflow. Perfect. So just confirm that it's called thumbnail and confirm inside of your procedures here in the video that you go to workflows slash thumbnail. So this will be quite interesting. I'm not sure which is the better resolution, this one or this one. I never know what's the height and what's the width of these things. Let's try one, right? So I'm going to go ahead and just try a very, very simple one. an image of Swiss Alps. Let's try that. Background job started. This may take some time. So we also need to like close this thumbnail. Let's work on that while we wait for our thing to happen. So thumbnail generates model here. We're going to go ahead and do the following. Let's do form reset here. And let's do an open change pulse. There we go. That's going to happen in the future. And let me just see. There we go. So run has started. It got the video and now it's generating a thumbnail. I'm very interested in this because this is not like my source code. I'm using a different model here. So I'm not sure if this will work. If it doesn't, I will just show you exactly what I have in my source code. So we're going to see what's going on here. I'm now going to pause and wait for this to finish. So it seems to be working. Let's go ahead and see what it created. I will refresh this and let's see if we have a thumbnail or not. Looks like we have a new thumbnail and it's so cool. Amazing. Looked like that was the correct resolution. Perfect. So we can now use AI to generate thumbnails as well. And here's what I'm interested in. It's my upload thing dashboard that I'm interested in. Is it getting properly cleaned up? So I'm going to go ahead and go inside of files here. And it looks like I have some things left here, but I'm not sure if this were in, this might have been left because of the 24 hour deletion thing. because remember I did delete some videos directly from my database, which doesn't trigger the web hook and then things get left inside. So this is how I'm going to test this one more time. I should have done this in the beginning. What I'm going to do is I'm going to remove all of my videos from my database and I need to have my DrizzleKit Studio running here. So let me just refresh this. I want to test it out because I want to make sure we are not doing any mistakes here. So I'm going to remove all of these videos here. I'm going to go inside of Uploading and I will remove all of my content here. I'm going to go inside of Mux and I will also remove all assets here. Simply so everything that I have is synchronized. And this one as well. And now let's go ahead and try this again. So into my content which is now completely empty I going to go ahead and I will simply upload a video What should happen now is that in a few moments an image should be generated and uploaded here There we go. Animated.gif and thumbnail.jpg. And if I now refresh this, I should see them here as well. And now let's go ahead and let's try to generate another one. So an image of aquarium. Maybe we can try that. And let's click generate. And one thing that we forgot to do in the thumbnail generate model is disable the button while this is going on. So disabled, it will be generate thumbnail dot is pending. Like this. There we go. And now if we've done this correctly, I should have another run started here. and after generate thumbnail, we have the cleanup or the cleanup already happened. No, so it's working on generate thumbnail now. Yeah, it's first generating it and then it's cleaning up. Okay, so it's working on that now. What I expect to see is that the thumbnail.jpg gets deleted. That's what I expect to hear, to see. and get replaced with my new one. So let me refresh again. Looks like it takes around 34 seconds. So this is a pretty good example of a long-running task, right? To generate a thumbnail, it seems to take 34 seconds. I'm pretty sure this is outside any timeout limit on some of the more popular hosting providers. So this function would fail on a majority of them. But thanks to our background jobs, you won't have to worry about that. Let's see. if we've done this correctly or not. There we go. Thumbnail has been replaced. And now we have this newly created AI aquarium image right here. Amazing. So in order to wrap up this chapter and in order to wrap up form section in general, let's go ahead and let's build a skeleton component so that when we load here, it doesn't have this, you know, loading dots here. So I'm going to go ahead and close everything here and go inside the form section. And we're going to go ahead and build the proper skeleton here. So just go ahead and import the skeleton. Of course, if this doesn't matter to you, you don't have to do this. You know, I want to give you a proper project. I want to give you the exact thing I've built. But if you think that you want to focus on some other things, you can just speed this part up or just copy it from the source code. So what I'm doing now, basically, is I'm just looking at what I did, how I built the original one, right? And then I'm just kind of mocking these elements with skeletons. Here's a hint. AI is extremely useful when it comes to creating these things. So feel free to ask AI to use it. So I'm adding a space too, and this is like the title and the description. And then I'm just going to add one big skeleton below that. So let's see how this looks for now. There we go. Not below that, next to it. So I'm just representing the button. No need for both the button and the little dropdown. Now what I'm going to do is I'm just going to copy like my grid structure here. Grid, grid calls one and enlarge grid calls five, gap six. Then I'm going to open my first column here. then I'm going to close it and then I'm just going to go ahead and add the first element of the form so this would represent the title and its input and then below that I'm going to represent oops let me just add it here I'm going to represent the description and its text area. There we go. And you know, you can choose how granular you want to go with here. So now I added a new one. And in here, I'm representing like the thumbnail. And in the last one here, I'm representing the category selection tool. There we go. So that part is finished. So if you want, you can leave it like this, Or you can go ahead and create another side. So I'm going to go ahead and do that here. And I'm going to go ahead and open like this gray box that we have, which renders our video, right? So now when I refresh, the box should appear here. I hope I did everything correct here. I think it's just not visible because there's nothing inside. So now I'm just going to add a skeleton with an aspect of video. And this will represent like this video here. There we go. That works fine. And then below that, I'm going to go ahead and open a div where I'm going to put all of these statuses, right? So this part will represent the video link like this. then below that we're going to do the video status and below that we can do the track status which are exactly the same and all that's left is the visibility component so we can do that one two three here like this there we go a very very detailed skeleton you don't have to build them like this of course But yeah, try out AI. It can help you with this kind of things. It can speed up this process very, very fast. Great. So you've learned all about background jobs now. And we've used Opstash's workflow, which is their new product, which I really, really like. Everything Opstash builds is always a very good developer experience. We know that already from their rate-limiting feature, from their Redis feature. And I'm sure this will only continue to go in that direction. I advise you to also explore more about background jobs. I think they're a very interesting solution. And I also advise you to maybe try for a challenge and change something. For example, inside of my, what's the name? Webhook. Inside of my videos, Webhook, right? Maybe change this. this entire thing where I upload and extract the new thumbnail key and the thumbnail URL, maybe move all of this to a new workflow and leave it to be a background job, right? Inside of here, you would simply do await workflow.trigger, and then just pass into URL to, of course, our process environment, ngrok, blah, blah, blah, slash API, and then maybe slash videos. And you can do, I don't know, however you want to name this route. It can be initial thumbnail or something like that, right? You can do that if you want to for challenge. Other than that, we are finished with the form section. And what we're going to work on building next is this, the video link. So we're finally going to go ahead and see this video. And then we can start building views, likes, comments, and slowly wrapping up with the search results page and the playlist. And I think that one more thing that I have here, which I did not show you, is that I keep this button disabled unless you change something. In case that's something that interests you, you can go ahead and find that button and disable it if we are pending or if not form state is dirty. Right? So when you initially come here, you cannot save anything, but if you change something, then you can save it in case you think that's a cool thing to do. Great. Amazing, amazing job and see you in the next chapter. So just to wrap this up, I love doing this part. We've created the thumbnail prompt, the generation workflow, and we've added the skeleton. And we've finished the entire form section. Great, great job.",
  "transcript_chars": 555267,
  "transcript_filled_at": "2026-06-06T16:03:44.528570+00:00",
  "transcript_filled_by": "tk-bulk-groq-retry-20260606"
}