Now let's create our info component right here. So I want to go back inside of my stream player and just below our header component I want to go ahead and add an info card. So let's go ahead and let's render the info card which we're going to create in a second. And let's prepare all the props it needs. So it needs host identity, which is user.id.
It needs the viewer identity, which is the identity which we get from our useViewerToken hook. It needs the name of the stream, which is stream.name. And it needs the thumbnail URL, which is stream.thumbnail.url. Like this. And now we're going to go ahead and create our infoCard.dsx.
And let's go ahead and mark this as use client and let's go ahead and create an interface info card props and let's make it accept the stream name thumbnail URL which is string or null host identity which is a string and viewer identity which is a string as well and now let's export const info card and let's go ahead and assign info card props in here and let's return a div info card. Now go back to the stream player and you can import the info card from .info card. I'm just going to move it here to the top and you should no longer be having any errors instead you should see the text info card or whatever you've written inside of the component. So now let's go ahead and let's style this. So first thing I'm going to do is I'm going to add...
Well, let's extract all of these things, right? So name, thumbnail URL, host identity and viewer identity. And let's define two constants here. So first const hostAsViewer is going to be host and then host identity and then let's define const is host to be viewer identity host as viewer. So we need this because only host will be able to see the information about their stream because this is also gonna hold the button to modify the stream information.
So if it is not host, return null like this. So in your case you should still be seeing this because you are on the creator page and you are a host of this stream. And now let's go ahead and give this div a class name of padding 6 actually let's give it a PX of 4 that's that's how much space we need this to take now let's go ahead and create a div here class name rounded xl and bg background like that and inside let's go ahead and render a div which is going to hold a couple of elements here so give it flex item center, Gap X 2.5 and Padding of 4 and first let's go ahead and render our so render a div right as I did and then a pencil icon inside so this div around the pencil is gonna be our little circle which we're going to create. So make sure you've added pencil from Lucid React. And now let's style this pencil by giving it a class name of H5NW5.
Now give this div a class name RoundedMediumBGBlue600PaddingOf2 and HALTONWALTO. Rounded medium BG blue 600 padding of 2 and H auto and width auto like this there we go so now you should have a nice little pencil inside of a rounded rectangle right now outside of this div which holds the pencil go ahead and open up a new div and actually it's not gonna have any classes but instead it's gonna render an h2 element edit your stream info like this and give it a class name of TextSmall, LargeTextLG, font-semi-bold and capitalize like that and below that a paragraph maximize your visibility and let's give this a class name of TextMutedPoreGround, TextExtraSmall and LgTextSmall, like that. So now, as you can see, we have a nice little box to edit our stream info and to maximize our visibility inside. So what I want to do now is go outside of this div which wraps the h2 and the paragraph and I'm going to add to do add a model button, right? And now I want to go outside of this div here and I want to add a separator and I don't think we have that so let's go inside of our terminal and let's add npx chat-cnui-latest-add-separator like this.
So make sure you add the separator to your project and then you can include the separator from ./.ui-separator like this and I'm going to change it to use the components like that. We are here And below that open up a new div with a class name p4lgp6 space y4 like that. And then we're simply going to create an empty div and inside we're going to render all the information we have about the stream. So let's create an h3 element which is going to render the stream name and give it the class name. Sorry, it's actually going to be a label which says name, right?
So hard-coded and give it a text small, text muted, foreground and margin bottom of 2. And then below that we're going to have a paragraph which renders the value of the stream. And give this a class name of text small and font semi-bold. Like this. Great.
And now you should see this. The name and it says Antonio's stream. Like that. And now I want to go ahead and copy and paste this div which holds h3 and the paragraph and paste it below here and in this one we're not going to render the name but the thumbnail like that and all I'm gonna do is instead of this paragraph I'm gonna check if we have the thumbnail URL we're gonna go ahead and render a div with a class name relative aspect video rounded and the overflow hidden width of 200 pixels border and border white slash 10 and inside we're going to render image from next slash image so make sure you import that and since we don't have anything in our database regarding the thumbnail URL you're not going to be able to see this but don't worry we're now going to create a model to modify this so we can actually upload an image. Let's give this a source of thumbnail URL and let's give it an alt of name of the stream.
Like that. Great! So now we have our stream info here and you can see the thumbnail here is empty right so exactly what we need and now we have to create a button that when we click on it it opens a model where we can modify our stream settings. So now let's go and find this comment which we left to add a model button and let's replace it with an info model component like this which currently does not exist and let's pass in some information so initial name is name and initial thumbnail URL is thumbnail URL like this and now go inside of the stream player here and create a new file info-model.dsx like that. Let's mark this as use client and let's create an interface info-model-props and this and it's going to accept the well initial name right which is a string and initial thumbnail URL which is string or null because this is optional.
Now let's export const info model and let's return a div info model here and let's go ahead and assign the props info model props. Now we can go back inside of the info card and you can import info model from slash info model and all you should see now is the text which says info model so let me show you that here. There we go just a big text which says info model. So we're now going to change this to be a button which opens the model. So for that we're going to need to import a couple of stuff from our add components UI dialog component which we already have installed because we use the dialog to generate our ingress and stream keys.
So you should be already be having this if by any chance you don't you have to run npx prisma sorry npx chatcny latest add dialogue let's import dialogue dialogue close dialogue content dialogue header dialogue title and dialogue trigger and make sure that you don't misspell this like this. Great! And now what we can do... Well let's also just add a button because I know we're gonna need it. So button from ./.UIButton but I will change it to components.
So go ahead and replace the div to render a dialog and go ahead and change this to use the dialog trigger give it an as child property because it's going to be wrapping a button and now render that button inside and say edit and give this button a variant of link a size of small and the class name of ML auto like this and now this should look better there we go So we have our edit and when we click on it it's going to open a model to edit the information. Outside of the dialog trigger add a dialog content. Inside add a dialog header and then a dialog title and render edit stream info inside of the title like this and now when you click on your edit button it should open a model which says edit stream info great so Now we can go ahead and add some more stuff. So I believe we need one more thing from chat CNUI here, npx chat CNUI at latest add label. I'm not sure if we have this, maybe we do.
I don't think we do. All right, so let's go ahead and now let's import the label component from ...ui-label and let's import the input component from ...ui-input and I'm already gonna go ahead and replace this from s-components instead like this. Great, now let's go below the dialog header and let's render a native HTML form element and let's give it a class name of space y14 and now let's create a div here with a class name of space y2. Let's go ahead and let's render the label which is going to say name and then below that we're gonna have our input component with a placeholder of stream name on change for now is going to be an empty arrow function value of name and disabled for now is going to be false and looks like I did something wrong with the name prop oh I did not extract this alright so this is going to be initial name here and we're going to go ahead and store our name in a state so we can optimistically update it so go ahead and import use state from react and let's prepare this so const name set name is going to be use state and the default value will be initial name like this and now we have the name here and we can safely render that as the value of this input right here great so what I want to do now is I want to go ahead outside of this div but inside of this form And I want to render another div with a class name of flex and justify between.
And I want to render the dialogue close, which is going to have a prop as child, because again, it's going to wrap around a button which will say cancel. Give this button a type of button because it's inside of a form so we have to explicitly define its function and give it a variant of ghost like that and alongside the dialog close another button here which is going to say save and give this button a variant of primary and a type of submit and let's also give it a disabled of false for now we're gonna change this later so now what should happen is that you should have an input which tells you what's the name of the stream and you have options to close the model or to save the model What we have to do now is we have to create an action which is going to appear here when we... Well first we have to actually control the value, right? So let's do that first. Let's do the onChange here.
So I'm going to go ahead and create const onChange to accept an event of react.changeEvent, specifically html input element like this and let's just do set name event target value like this and then we can use that on change and replace it here so just passing on change like that great so now we have that what I want to do now is I want to create an on submit function here, which accepts the event, which is a type of react.formEvent and accepts the HTML form element. And now let's do prevent default here. And what I wanna do now is import useTransition from React. So we already have that. So let's add it here at the top and I'm gonna go ahead and use that here so use transition and let's extract is pending and start transition and then in here we can use start transition and we can simply call update stream from actions stream so we already have this action the reason we have it is because we use it to change those values isChatEnabled, isChatFollowersOnly and in here we already prepared that name is one of the values we will be able to modify using this server action so this is actually ready for us to use we don't even have to create anything so inside of the update stream let's go ahead and pass in the value which is an object of name which equals name and now let's go ahead and write a couple of success messages so .then is simply going to say toast from sonar so make sure you import this package I'm going to move it here so toast.success stream updated and catch is gonna be toast.error something went wrong like this And now let's use this on submit by adding it to our native form on submit.
On submit. Like this. And this should already be working. So I'm going to go ahead and refresh this. And I'm going to click edit here once it loads and I'm going to change this to something else and I'm going to click save and we forgot to add the pending states but I think this should already be working and it is Now here it says something else and here it says something else.
Great! So now what I want to do is I want to use this pending prop here and I want to disable the input while that is working on. So yeah, we already have it hardcoded. So remove the hardcoded one and let's do it here. It's pending for the save button.
And if you try again it should be a better experience because now Another change and when I click or you can click enter and there we go This is disabled and the button is disabled Perfect. And one more thing I want to do is I want to close the model once this finishes for that I need to import two things from React use ref and element ref from React and let's go ahead and add const close whoops, const close ref to be useRef with the default value of null and its specific type will be the element ref specifically button like this and now let's go ahead and let's assign this closeRef to our DialogClose component so find the DialogClose and give it the property ref of closeRef and now let's extend this .then so not only does it show the toast message but it also closes the ref right? So what I want to do is write close ref question mark current clicked and now we should have an even better experience so 1, 2, 3 I click save and there we go you can see how it updates in real time perfect so what we have to do now is we have to go ahead and implement upload thing so that we can upload a nice image here.
So I want you to go to upload thing dot com or just Google upload thing and go ahead and sign in. And in here you can see that I already have a couple of projects what I'm gonna go ahead and do now is click create a new app and I'm gonna give this app a name Game Hub. We can leave the app URL empty and just click create app like this. Great! What I want to do now is go inside of my API keys and I want to go ahead and copy both of this.
So let's go ahead and copy that and let's go inside of our .environment file here. After we add LiveKit let's add UploadThingSecret and UploadThingAppID. So make sure you have those. And now let's go in the overview and let's go ahead and click Read More for the Getting Started. So in here you can of course read more about how upload thing works, what it is exactly and all the stuff it does for you.
So the reason I like it is it's extremely fast to implement. Of course, it will be interesting to see how we could implement S3 directly, but this is already a 12-hour tutorial, so this just saves me a lot of time and it's completely open source and you don't require a credit card whereas with AWS you do require a credit card, right? So it allows more viewers to do this and to be honest it's simply amazing. So you're going to see how it's going to generate type safe components for us based on the routers which we will define on our backend. So that's something that other upload providers don't do.
And this is open source and it does that and it was created by a person you might know called Theo you might even follow him on YouTube so let's go ahead and click on the sidebar getting started next.js and we are using the app folder so let's click here So we have a command to run so let's go ahead and copy this and let's go ahead inside of our terminal here and I'm just gonna go ahead and paste that. Let me expand my screen as much as I can so we are doing npm install upload thing and add upload thing slash react so make sure you add those two packages and let's leave that to install now it says to add the environment variables so we already did that so we didn't have to and now we have to set up a file router So for that we can simply copy this entire function and then we're going to go ahead and modify it so it says to create it in app, app API, upload thing core.ts so let's go ahead and create that exact folder structure so I'm going to close everything let's go inside of app API let's create a new folder upload thing and let's create a file core.ts and inside I'm going to paste this entire code snippet that you can see from the documentation here.
You can of course visit my github directly if for any reason this has changed in the future. What I'm gonna do now is I'm gonna get rid of all the comments because we're gonna go ahead and modify this and it's just gonna be easier to look at this at least for me, right? For this video I mean, right? Okay, so I just got rid of all the comments and I'm actually gonna go ahead and get rid of this function. So I'm just gonna go ahead and have this empty our file router and first thing that I want to do is get rid of this out function because you know it's a fake out function and what I want to do now is add a thumbnail uploader here which is gonna be function from create upload thing here and it's going to accept a single image and let's give it a max file size of 4 megabytes and let's give it a max file count 1 like this.
So make sure you have these two items inside of your thumbnail file uploader config. And let me just collapse this even further just so it looks a bit better. Like this. Great, and now let's go ahead and let's chain this with a middleware here so inside of the middleware it's going to be an asynchronous function in which we're going to get self from await getSelf from libauthService so make sure you import libauthService at the top here and now let's go ahead and let's also extract the metadata and the file from this asynchronous function sorry no that doesn't exist here my apologies So middleware is just empty like this and we just return user self like that. And then let's chain onUploadComplete like this and that is going to be asynchronous and that is going to have the metadata and the file like this.
So let's go ahead and create this function here. We're going to do something very simple. We're simply going to update the stream based on the user in the middleware. So await db.stream.update and we have to import db so let's import db from s-libdb. Let's pass in the where to be user ID from metadata.user.id and let's change the data thumbnail URL to be file.url like this and now we choose what to return back to the client so I'm gonna return an object file URL to be file.url like this there we go we created our own upload endpoint right so let's see what we have to do next so we just finished this and now it tells us to create the next JS API route using the file router so it tells us to create that in the upload thing folder route.cs So let's copy this simple snippet from here and let's go inside of UploadThing and create a new file route.cs and let's paste this here like that and we already have .core so no issues here.
Perfect! So make sure this is your upload thing route.ts file and now we have to add upload things styles. So make sure you select the tab Tailwind like this and now we have to revisit our tailwind.config.ts like this. So if you follow the instructions from the beginning of the video you should only have one file tailwind.config.ts like this. So if you follow the instructions from the beginning of the video you should only have one file tailwind.config.ts like this.
So if you have two files here's what you can do. You can simply go inside of my github, copy this entire file, paste it in tailwind.config.ts and remove your other file. You can simply do that but if you follow the instructions in the beginning where we resolved a little conflict with chat.cn and tailwind you should only have one file. So now I'm going to add this import here. So upload thing slash Tailwind.
And now we have to go ahead and wrap our entire project in that. I mean our entire config in that. So I'm going to go ahead and wrap the entire thing here and go all the way to the bottom and add an end line here like this. And this is something that happens in your keyframes. You're now gonna have this error.
But here's the thing. We don't use any components which needs this. So I'm pretty sure this is something that might be resolved soon. I don't know what this is to be honest. But we can simply remove the keyframes object like this.
And then we can also remove the animation because those two are connected. Like this. And that's it. That's all we need. Great.
And now, let's go ahead and let's create the Upload Thing components. So inside of Source, Utils, Upload Thing, I'm gonna use the lib folder instead. So I'm gonna copy this simple snippet, and I'm gonna go inside of my lib and I'm gonna create upload thing.cs and I'm gonna paste a simple snippet here and I will replace this little, this, whatever this is with at, because that's our alias like this. So add API, app API, upload thing core which we just created recently you can even hold ctrl or command and click on it and it should open your file router. Make sure you save this file and now we have three new components which are type safe against our file router meaning that they're gonna have sense of this information like our thumbnail uploader and our image configuration.
Great! So what I want to do now is I want to go back inside of our info model so app folder, sorry components, stream player and in here we should have the info model where we added an input to change our state here and what I want to do now is just as we created a state for our name let's copy and paste this one and make it thumbnail URL and let's change this to set thumbnail URL and let's use the initial thumbnail do we have it here? Oh I forgot to extract it from the props like this initial thumbnail URL and just paste that here great so now what I want to do is I want to go ahead and import upload drop zone from that lib that we just created. So let's go ahead and let's import upload drop zone not from upload thing react but from add slash lib upload thing because that's our abstraction around upload things. So make sure you do this import and now what we can do is we can go ahead and find where we have this div which wraps the label and the input and go outside of it and open a new div like this and write a class name again space y2 Render a label and write thumbnail and then below that let's go ahead and let's render our drop zone.
First I want to wrap it inside of a div which has a class name of rounded extra large border outline dashed and outline muted like this. And now let's render the upload drop zone which we imported from the lib folder. And here's the cool thing. We can pass the endpoint now and as you can see it auto completes to our thumbnail uploader which is defined all the way in our API so we completely own this endpoint. And now let's give it an appearance prop.
And let's change the label, use the color FFFFF. Let's go ahead and write a loud content to use the color FFF, FFF as well and let's pass in the prop on client upload complete. Let's get the response. Let's do set thumbnail URL to be response first in the array dot URL setThumbnail URL to be response firstInDRA.URL and let's do router.refresh so we have to add the router so import route sorry use router from next navigation and let's go ahead and add the router so const router use router and then simply here in the thumbnail let's go ahead and render router.refresh here like this, Great! So I believe already everything should be working.
I think we've finished the entire thing. So yeah, this should be it. Let's go ahead and refresh this and when I go ahead and click on edit here it's loading and there we go. We can choose our files to upload. And what I want to do now is I want to go ahead and open my terminal and I'm gonna run npx prisma studio.
So npx prisma studio. Make sure this is open and now if you look at your stream model both of your streams should have no thumbnail URL. So go ahead and upload an image, I'm gonna go ahead and I'm going to select a specific image from here. Alright, so I just took a random screenshot and let's click upload one file. And let's wait for this to upload.
And perhaps it's not gonna work now and that's because we forgot to do one thing and that is to add this to the middleware so let's go ahead so yours is probably stuck on loading as well I believe we might have an error we do right So we have an error here in our terminal. So let's do the following. Let's go inside of our middleware and alongside this two routes, let's add slash API slash upload thing. Like this. And then let's go ahead and refresh this page and let's try uploading a thumbnail one more time so I'm gonna click here I'm gonna go ahead and select the random screenshot that I took here and now it should be working So after this uploads it should just reset to the initial state I believe.
There we go and I also have a little error here so we're gonna resolve both of that but first I want to go ahead and refresh my Prisma Studio and in here I now have a thumbnail URL. So it's successfully uploaded. Great! And you can see the exact process inside of app folder API upload thing core right here. You can see that Once the upload is complete we use that file and we update the stream in our database using the currently logged in user.
So the user doesn't have to even confirm that, right? We immediately save the image so this way we reduce the amount of uploads and we immediately use them. And now we have to resolve this little error here. So look at this error and if you even have it, right? If you don't just follow along and do what I do.
But if you have this error, go ahead and look at this specific hostname in my case is utfs.io and go ahead and copy this hostname right here and you're gonna have to go inside of your next config so let's go inside of next.config.js and above that add images, domains and just render one of those, like this and now what I recommend you do is that you shut down your terminal where you do npm run dev and do npm run dev again like this and just refresh this and what we're gonna do now is we're going to render that image so head back inside of components, stream player and you should have the info model here. So we're gonna go ahead and we're gonna make this dynamic. So there we go, you should already see it here. Right? In here it already appears and it looks like it's a bit stretched.
So let's go ahead and find not the info model but info card and find where you rendered the thumbnail and go ahead and give it a class name of object cover like this and there we go now it looks just a tiny bit better. Go back inside of the info model now and now what I want to do is I want to make this dynamic right so I'm going to go ahead below this label and I'm going to wrap this entire thing in a ternary so if we have thumbnail URL which we store inside of our state here. So if we have thumbnail URL, we're gonna go ahead and render one thing. Otherwise we're gonna go ahead, whoops, we're gonna go ahead and render this, right? So this entire div which holds the drop zone like that and in here we're gonna go ahead and render a div with a class name relative aspect video rounded Excel overflow hidden border and border dash white slash 10 and let's do a div with a class name div with a class name absolute top two right two and z ten like that and now let's go ahead and let's render a hint component so make sure you import that from dot dot slash hint or slash components hint I'm just going to move it here.
So let's go back inside of this first ternary and let's give this hint a label, remove thumbnail. And let's give it an as child property and side left. Now let's go ahead and render a button component inside which is going to be our trash icon from Lucid React. So make sure you import trash from Lucid React. I'm just going to move it here and let's go ahead and give this trash a class name h4 and width of 4.
Let's give this a type of button. Disabled is going to be a spending. Let's give it an on click for now to be an empty arrow function and let's give it a class name of HAuto and width of Auto and P 1.5 like that and then outside of this div which just wrapped that little hint and our delete button we're going to go ahead and render that image so make sure you import the image from next slash image I'm gonna move that to the top here all right and let's go ahead and give it a source of thumbnail URL let's go ahead and give it an alt of thumbnail, let's give it a fill property and class name object cover like this and let's try this out now. So when I go ahead and click on edit, there we go I have a nice little delete button and I have a render of my image which I uploaded. So now we have to create this remove a thumbnail function.
So let's go ahead and create a server action to remove a thumbnail. So I'm gonna go ahead and go inside maybe we can actually reuse stream perhaps we can do that as well let's try it out let's do thumbnail URL to be values dot thumbnail URL like that Let's try and add that to our valid data inside of our stream actions. So I'm going to attempt something here. So if we send undefined that's simply not going to pass that to be updated but I think that if we send null that then it should be changed to null but I'm not entirely sure we're gonna try it out now so let's go inside of stream player we have the info model component and now we're gonna create this which is currently empty right So just as we created our onSubmit function, let's create const onRemoveThumbnail. Let's just call it onRemove.
And in here, I'm going to call startTransition. So we already have that, right? So we can reuse it from this is pending and start transition and let's call update the stream and I'm going to pass in the thumbnail url not to be undefined but to be null specifically so let's see if that's gonna work and let's add a toast whoops toast dot success thumbnail removed and let's add dot catch toast dot error something went wrong like that And now let's copy the on remove here and let's put it in place of our trash button here. So on remove. And I'm really interested to see what's going to happen now.
So in here in my Prism Studio I have a thumbnail URL for my stream and if I click here let's see if that's gonna remove the thumbnail or not. So after I refresh... Oh it looks like it does remove it. So let's go ahead and refresh this just to confirm. And it did remove it.
Great! So that now works. But you saw that in here the thumbnail stayed. So what we can do here is we can extend our .then for the onRemove like this and we can go ahead and do setThumbnail to be an empty string like that and we can also go ahead and do but we can close the dialog right because this kind of counts as a save action yeah I'm gonna do this so close ref current click like that so let's try this again I'm gonna go ahead and refresh everything here. Okay, let me expand this.
I'm going to click on edit. I'm gonna go ahead and upload my screenshot here. I'm uploading the screenshot and now this should go without any errors here. I hope. There we go.
And let's go ahead and close this. So it's right here. Great. Let's go ahead and refresh this. There we go.
It's still here. And now let's try and removing this thumbnail and that should close the model and there we go it closes this as well and we can also close the model once we upload the thumbnail because that's also practically a save action We know that it's immediately in the database so not to confuse our users that they have to click save. So let's go ahead inside of our drop zone and in here we call router.refresh. Let's also do close ref current click like this and let's go ahead and try this one more time so we wrap this module up so I'm gonna upload an image I'm going to click upload one file and once it uploads it should close the model immediately Let's see if it is and there we go we now have our stream thumbnail and the same experience is if we remove the thumbnail because that is reflected in the database in real time. Perfect!
So now you have the ability to change the name of the stream and to add a thumbnail. What we're going to build next is the About You card where you can modify your user's bio.