Build a File Storage App with Role Based Authorization (Next.js, Shadcn, Typescript)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey everyone I'm excited to share with you this full stack tutorial that I've been working on where we basically build out a Google Drive mini clone so I'm going to walk through all the features and show you what I built and then you can watch the entire tutorial and see how I built this so if we go ahead and just click get started we don't have an account yet so that will ask us to create one we'll go ahead and sign in again we are using clerk for authentication which is one of the sponsors of this video and I'm also using convex for the backend as a service I like both of these Services a lot they really help you build faster and I will go ahead and log in for my okay and I'll go ahead and click continue here and so after logging in you get redirected to your dashboard I already have an account and I uploaded some files but some of the functionality that you're going to learn by watching this tutorial is we have the ability to upload new files let's just go ahead and say another file here go ahead and select this we do have some error States built into this so if you don't have a title here it'll airor out and then let's just go ahead and select a image go ahead and submit that and after submitting you'll see that we get a toast here at the bottom again the entire UI and all the components are built using Shad CN so we're going to learn how to use that and then the front end is built with nextjs so after uploading the file some functionality that you have is you can actually search over your files by name so if I type in like another file and hit enter that'll filter down to these files that we have in our system you can also switch from a grid View to a table view if that's something that helps you visualize your files better let's go back to grid view another thing we added is a filter based on types if I go ahead and search by everything I can search by images I can search by CSV I can search by PDF and then Additionally you can go ahead and Mark files for deletion so for example if I want to mark this one as deleted and how this works is we have a convex KRON job which runs every minute and it's going to run through and just delete anything that you have set up and marked ready for deletion but you also have the ability to restore it so if you wanted to kind of stop that process you can restore that image and it'll go back to your all files right there you go the file has been deleted another thing we added was the ability to favorite certain files so like let's just favorite these two images and go to our favorites subcategory notice that these show up here and we can just go ahead and unfavorite them if we want to we also have the ability to download these files so let's just go ahead and click on this I'll click download notice that they get added to my downloads directory images will open up over here in a new tab and then PDFs again will open up in a new tab as well this PDF is just blank but if it was actually defined you'd see that PDF show up another thing that's really cool that I'll show you how to build out is we have organizations right so once you create an account you can switch between an actual team based organization or you can go to your own personal account here so this is my personal account all my files will be completely isolated from my other accounts so the organization feature is built directly in the clerk I didn't have to build any of these components or this modal that's popping up so I can actually go here and I can invite new people to my project and I can select what role they want to be so if you're a member you don't have access to delete files you just have access to upload files but if you're an admin you have access to delete whatever you want okay and so as an admin I have access to delete any file that belongs to me and other files that exist on this organization but if I were to go and switch a user from no longer being an admin to being a member now I don't have access to delete their files okay so I have some type of role-based authorization set up to kind of hide things in the UI and also prevent the backend from ever allowing certain actions which is pretty cool but of course you still have access to delete the files that you upload and then of course we have the ability to manage your account where you can come in through here um if I want to basically change what my profile picture looked like so if let's just go ahead and grab a screenshot here and go ahead and upload this click continue click finish you'll see that my image updates down here and now my web dev Cod icon is gone and now I just have a screenshot okay so that was all updated live and like I mentioned all the backend is written using convex which is a realtime database so basically if I were to go to any of these files and modify the data in the convex dashboard let's just go ahead and go here and say like hello world go ahead and save that go back to the UI notice that that instantly updates because of the way websockets are hooked in the convex we get live update events as our data is changing it makes our code a lot more concise that we have to write and much easier to have a really live great user experience so this is a longer video hope you guys have time to watch through it and learn something new I think you guys are going to learn something really good I really enjoyed working on this tutorial so I hope you guys enjoy it as well let's just go ahead and jump into the code and get started all right let's just go ahead and get started I'm going to create a new repository for this project and I'm going to call it file Drive okay keep it short and sweet and then I'll say MIT we make this public so throughout this tutorial I am going to be doing commits so if you ever want to go back to a particular part in the tutorial you can go and check out that commit or if you want to skip ahead and like bypass all of this setup stuff because you're a little bit more advanced you already know how to set up an xjs project in convex and clerk you can just go ahead and check out directly to commit if you want to all right let's go ahead and clone file Drive over to my workspace folder so I'll say get clone and then I'll paste that URL in and that should set up a directory called file drive I can go ahead and reopen that with VSS code and we could probably start coding so we're going to start with setting up a nextjs project if you go to the convex docs and go to the quick start guide they have a nextjs section so let's just go ahead and run MPX create next app latest and instead of putting a project name I'll just do dot it should just kind of use file drive for the project name and that'll ask you a couple of things of like how do you want your next JS project set up we're going to use the defaults for everything the typescript Tailwind Etc all right so the next steps is we need to install convex right so this is an mpm package you just go ahead and install it go ahead and run this that should install the convex package and now to initialize your convex project just run MPX convex Dev and if this is the first time running this It'll like ask you to sign in and get this all set up but I've already done that so it's was going to say hey we notice that this is a new project let's just go ahead and say yes we do want a new project and I'm going to put it as part of my web dev Cod team there's the project name and that should take your project and get it set up in convex so now if we go back to our convex dashboard you'll see that that project just showed up automatically for us and we can click it and this is a dashboard that we're going to use as we're developing out our convex application there's a lot of things in here that we'll kind of uh use in the future I'm not going to dive into them just yet but just know that this is a very useful dashboard to do your development and to kind of run functions manually into to inspect your data as you're creating things so now let's just continue down we do have to create this convex client provider so inside a source app I will go ahead and make that convex client provider and uh we'll just copy over this code so let's just click this copy button and paste that in and so we're going to use this provider inside of our layout and kind of just wrap all of our component so it has access to various comvex books um if we were to go down here to the they give an example how to do it basically import the provider and wrap the children so we're going to go ahead and do that okay and then we'll autoimport that but I do want to verify that we have next public convex URL set up so if we go to ourv local you'll see that convex already put that there and that's kind of the tell our react app how to connect to our convex back in so we can kind of start doing queries and mutations to store data so second thing we're going to do is I want to make another terminal here and I'm going to run our next app so run Dev that's how you can spin up your next app go ahead and rename those terminals so now if we go to Local Host 3000 you see our next app is set up so cool so I think we're set up on that aspect the next part is we do want to set up authentication if you go to the comvex documentation they do tell you how to set up with clerk which is what we're going to be using so I'll type authentication here I'll go to Clerk and they have a setup guide for nextjs so going to the clerk dashboard I'm going to go ahead and create a new project and I'm going to call it file drive and we're going to keep it with Google and email for authentication for right now just to keep it simple but they do have a bunch of different social media signin options if you ever need to opt into those in the future it's very easy to get set up so let's create our clerk app and then we're going to copy these API keys and we're going to put them them in our EnV local as well so go and paste those in the next step to integrate clerk with convex is you have to go to this jtbd tokens and you can just go ahead and click on this convex icon it already has a first class integration with convex which is nice and so we're going to copy this issuer key here copy it and then just click apply changes right so using that issuer key we need to go to convex here and we're going to make an off. conf. TS file so say new off config TS and we're going to go ahead and paste in that link so I don't lose it from a clipboard but then using the convex docs we're going to go ahead and just paste that in here into this object so this is going to tell clerk how to phone home when we get a token and authenticate it against clerk basically because we need to use clerk for authenticating the front end like maybe showing some Widgets or hiding some widgets but also when we make API requests we need the verifi that the user making the request has a correct clerk token and um should it be authenticated to do that request right and then finally we need to install clerk instead of clerk react I'm going to install clerk next as a different package since we are using nextjs so I'm going to run inpay install clerk nextjs that should give us the libraries that we need okay and then moving on to step eight we have to basically wrap our convex provider with this clerk provider so let's just go ahead and copy these all right so let's open the convex client provider and I'm going to paste in that example code here and we're going to autoimport the clerk Provider from Clerk nextjs and then also the convex provider with clerk now the main thing that needs to change here is instead of rendering out app we have to render out children and delete that I don't remember what to put here so let's just go ahead and do EnV local and I believe it's this next public clerk publish key process EnV and I'll type that in and then over here for use o I do believe we have to import that as well so although we have the clerk package kind of installed and we wrapped our application with the clerk providers and the convex providers we do want to make a middleware file so that it can kind of authenticate our endpoints faster well middleware dots and I'll go aad and paste that in and the way this works is you have this matcher that basically says every single endpoint I just want you to go ahead and make sure that you're authenticated and if you want to make a couple of endpoints that are public you can make a public Scout directory here and I'll just go ahead and put a slash okay so that's tell clerk that hey someone can go to the dashboard and don't like redirect him to a login page and let me show you what happens if I don't do this so let's leave that blank refresh the page here and that should redirect us to a clerk login page which might not be what we want for a landing page right so let's just go ahead and make that a public route and then I'm going to go ahead and go back I think that finishes the setup for convex in clerk but the very last thing I want to bring in is shad CN Shaden is the component library that I like to use in all my applications I find it really nice and really fun to to use so let's just go ahead and install this we're going to skip this command because we already have a next project set up but you can install Shad CN by running this next command here so go and run that and this is also going to have a questioner that asks you some stuff so I'm going to do is the default style you can change your base colors I'll just keep it as slate I'll say yes for CSS variables and there we go it should be set up by the way if you haven't used Shad CM before they have all these components you can basically just install into your application by running a CLI command um but they also have a examples link here that has like fully worked out applications where you can kind of view the code and you can pick and choose and bring stuff in for example this work CU is something that I've actually brought into another side project that I work on I it's super easy to kind of just go to GitHub repo and copy code over into your project and have exactly what you're seeing here so now that Shad CN is installed let's just verify that everything's working with it so what I'm going to do is I'm going to go to components here and we're going to find a button component and the way you can install different components is you basically just run this command in your project and that'll pull in a button component and it'll put it in a components directory over here so notice that we have a button and if we were to go to our main page and probably delete like all this boilerplate neck stuff let's just delete all that what we're going to do is I'm going to add a button Auto Import that and say hello world and let's verify that this shows up on our application there you go so now Shad Cen is hooked up correctly you can click this and hover over it and do stuff so we verified Chad sc's working let's also verify that clerk is set up properly the cool thing about clerk is that they provide you a bunch of components that are kind of working directly with nextjs from the get-go so we go back to all docs and we go to components you'll see see they have like a signin component they have a signin button uh if we just were to grab this signin button and put it on our application we should automatically get a modal I think there's also a mode here go and import that I think there's a mode and you can say modal if you want to and so this will give us a button that the user can click and then aoto will pop up and they can actually sign in right in this modal right so this is all built into clerk I didn't have to write this modal it's pretty cool let's try signing with Google and I'll just use my webdev Cody account but we just signed in with the modal and we got redirected back to our homepage but how do we know if we're actually signed in like we should probably hide this button and show like a sign out button if we are signed in so one way you can do this is you can say const session is equal to use session like this and I'm actually going to convert this to a client component because I do believe use session has has to be used in a client component so going back to clerk let's look at some of the other components that they give you notice that they do give you quite a bit but there's one that is called signed in right so if you want to actually dynamically show things on the page based on if you're signed in or signed out you can just use this component and wrap different things on your page so for example let's just bring in that signed in component from Clerk nextjs and we're going to go ahead and just wrap our signin button um actually this needs to be a sign out button so we're going to actually copy this I'll call it sign out button like this and then we're going to go ahead and say signed out from clerk next like this so those components basically abstract away looking at your session to see if you're logged in or logged out and then it'll show whatever you want based on that so I can go ahead and click on sign out and that should delete our cookie and like delete delete our session um but notice that it's not really styled we do want this to be styled like the Shad Cen button so we can actually just take this button and we can put that inside as a child and I believe it'll already be um let's change this to sign out and then let's also go over here to the sign in button like this paste that in I'll say sign in okay so let's click it that should sign us out click sign in we get the modal and we can sign back in awesome so Shad see in the setup clerk is set up convex we need to actually start doing something in convex to verify authentication is all correctly hooked up into convex so let's create our first query in convex and typically the way you name your files in convex is your files should map one to one with your data models or your tables and so since we're making like a Google Drive type of Clone it would be nice to be able to store f files okay so let's just make a file called files. TS and inside of this typescript file we can just say export const create file and we're going to say that's going to be equal to mutation which we're going to have to import from um server oh if I have to put do slash there okay and so what a mutation is it's basically an endpoint that you can call from your front end react code and it's going to do some type of modification such as store an entry into the convexes database there's also a concept of a query that's basically if you need to fetch data from your convex database and then there's a third thing called an action and you use actions if you ever need to contact a third party library or a third party service such as like open AI um but we won't cover that just yet so when you make a mutation you can pass in some args I'll keep them blank for right now and then you also need to define a Handler okay I'll put a sync in front of there and so how the Handler works is from the front end you can pass in whatever type of arguments you want and then you can take those arguments you can do some validation on them some a validation is actually built in directly to convex if you want to so for example if you want to store like a name so like a file name I can go ahead and bring in V from convex values and we will call this a string and so now this is a mutation that we can call from the front end we have to pass in a string okay so in this mutation basically we're going to pretend like there's a form where a user can type in a name of a file and also maybe like select the file to upload that the convex has a database built in and we can basically say await context. DB do insert and then we can name what table we want in our case you can just put this whatever string you want we'll call it files but you can name it whatever and then finally you want to store the name so I'll just go and say ar. name okay so now we can actually invoke this from our front end we were to click a button or or something we could just have it call this U maybe we should do that real quick so let's go back to page. TSX and we'll just add a quick button here it has an on click and we need it to do something so I'll just call it click me for right now but the way you can actually pull in these mutation functions on your front end client components is you say const crate file is equal to use mutation and then we're going to bring in api. files. create file how easy that was you get all this nice type safety with convex out of the box so now from our front end let's call create file so down here if you were to click this we're just going to go ahead and call this method we're going to pass in name of hello world okay so going back to the UI let's see if this all works I should be able to click on that click me button and that should have sent a request to convex using a websocket event and if we go over to convex and look at our data notice that files automatically got added dynamically and then we can see that we have that hello world so one really cool thing about convex is everything is live right so as you keep on adding files this will automatically update for you instantly vice versa if I were to go over here and change some values and we had that displayed that would update in our UI as well so but I'll show that in a second um we did get everything hooked up we have a mutation that's being called from our front end you saw right there we just stored a an object that had name of hello world the second thing I want to kind of introduce you to is something called a query so we showed you a mutation that's for creating and storing data into your database how do you get that data back out so let's just go and say export const git files is equal to a query and then that's going to take an args of empty object and then also a sync Handler context args like this and we're going to go ahead and say return context. dbquery files and then will say collect and that's how you can basically fetch all of the entries that were stored in that table and send them over to the front end right so the last step is we have to call this from the front end so let's go back to our page TSX and patterns very similar you just basically say con git files is equal to use Query API files get files now the difference is if you had arguments you could pass those in in our case we don't need to so let's just go ahe and leave it blank but if you hover over this you'll see that this is an array of anything so that's basically going to grab everything from that files table and send it back to us and we can actually display those um and I'm actually going to rename this as just files I don't know why I call it get files let's go down here I'm going to say files at map and then for every file we get back let's just return a div that has maybe a key file underscore um ID I believe will exist on that and then we can just go ahead and say file. name okay probably need to do a money sign here and it's not maap it's just maap okay so now if we go back to our UI notice that hello World shows up but if I already to keep on clicking this button notice that we keep on creating data convex is going to store that data into its database that triggers off a notification to send a websocket event to every query that's listening to those rows or those tables that have changed so every client that's connected that's listening for that files table is going to get that update and that'll change right so I can actually have like multiple tabs open like this and as I'm clicking on one like you'll see the data pop up over here as I click over here you'll see it pop up over here and then also I can just go ahead and just delete all these and you'll see them clear out on the right so there's the whole like live data real-time database aspect of convex which I just wanted to kind of demo for you all because I do think it is a game Cher that makes writing UI is very very easy um so one thing I want to to point out is that like when you hovered over the types of files it just says any of array not the best right so convex has a way to create a schema like this and inside the schema you can actually Define what your tables look like right what fields are required what properties are required on those fields Etc let's go to data over here and I'm actually going to click on this button at the bottom that says show schema and convex is going to give you a recommended schema based on what you're table already looks like right so I can just go ahead and grab this generated schema and then I'll paste that in here and now if we go back to our code and hover over files notice that it's type safe now our front end knows exactly what our backend is going to return to us and what that data is going to look like because we're just returning stuff that was directly from schema so that's what we're going to do going forward is we'll start defining the schema and I promise there's just one more thing I want to talk about that is how do you get authentic ation working on the backend side of things right with convex we have to know if the user who's making that request is logged in you don't want just anyone creating files and fetching files right so in order to know if someone is logged in you can actually just look at this context so I say context Dot and then off get user identity so I'm going to say const identity is equal to this I'm going to wait on it and then if we were to console log or just to Identity dot because I think it's type safe no notice that we get back all of this information okay so if identity is defined we know that the user is logged in and they have been validated against clerk because of that off config that we set up if it's not defined then we know that they're not logged in okay so for example if someone tries to create a file we can say if there is no identity then we're just going to go ahead and throw new convex error like this you must be logged in to create a file or upload a file or something okay so let's let's verify this I'm going to go ahead and just go back to our app and we are not signed in right now okay so if I were to click this notice that the back end throws an error it says convex error you must be logged in to upload a file so now we've actually authenticated our back in mutation which is great we don't want anyone just uploading files and secondly when someone tries to fetch all the files they should probably be logged in as well so we're going to take this approach and we're going to do something similar to the query we're going to get the identity and say if there is no identity then we're just going to return an empty array okay so that the UI doesn't just like crash for people all right so there's a setup with everything and a walkthrough of just getting some mutations and queries written in convex getting this set up with nextjs shad Cen and clerk I'm going to go ahead and commit and I'll just say initial setup of project and sync that up all right so at this point I think there's a couple things we could do in the UI to Spruce it up and make it look a little bit better so first of all we could probably get a header up here that shows an avatar if you're logged in or logged out let's try doing that first let's go over to our layout and inside the layout we want a header to exist on every single page so we could just above the children we could pretend like we have a header component and then we're going to go over here and we're going to create one so say header. TSX and we will just go aead and export function header and then we're going to return to div and we're just going to style it a little bit so I'll say class name is equal to and we'll give it a border bottom and inside of this header there's a couple things we could do first of all we could probably put a container so that the entire thing is kind of centered on the page I'll say MX Auto and that'll Center it and I'll just say hello world for right now and we're going to go back and we're going to import this header in the layout now we have a header showing up so what I want to add is you know in most applications in the top right there is like an avatar and then you click it and it gives you more options that's actually built into clerk in this user button so if we were to grab this user button go ahead and put it up there we should be able to see our name you click on it you get a drop down of signing out and managing account you click on this you get a modal that you can actually just use to do more security um configurations on your clerk user here you can change and update your image you can remove your image yeah so we just want to Center this and get it on to the far right and we're going to go ahead and just here we'll say justify between we'll give it a flex and I think that should push it left and right and then on the header itself let's just go ahead and give it like a padding y of four and then BG gray of 50 so it's just a little bit different from the actual body and then last thing this isn't really centered vertically so if you're familiar with flex box all you need to do is say items Center on any Flex box parent and then all of the children will be centered vertically like this so it's already looking like a real application right we got a header looks pretty good now for hello world I think we should put like a you know a logo or something so we could actually just call this like file Drive okay if we had an icon we could put that up there we won't worry about making it all perfect right now the second thing I plan to do is is we want to add organizations to this file Drive app we're building right clerk has organization support built into it and again if I were to just grab in this organization switcher component we could put that directly next to our user button okay just go ahe and paste that in and I'll import that and I think on here let's just go a and give this like a flex gap of two so there's a little bit of spacing between them now let's go back to the UI now right now it's not showing showing up and the reason it's not showing up because you actually have to enable organizations on your clerk application so let's go back to clerk here it is and then we're going to go to organization settings and we want to enable it okay there's a little toggle switch over here that my head is hiding but if I click on this that'll turn on organizations and now you can actually have users create their own organizations and invite people to it let's go back to the UI and just refresh it and we should see an organization drop down so now if you click this you can actually go and create an organization so you can have multiple people have access to the files that we plan to upload in our application right so I'm going to go ahead and call this my awesome team and I going to create the organization and then you can actually invite other people to this organization which will send them an email I believe and then they can kind of log into your application as well or and you can also select if they're a member or an admin I think admins are able to like delete people from the organization Etc I'm going to skip this for right now just go and say skip but now we actually have my awesome team organization and what we plan to do is you're going to have an org ID that we're going to attach to our data right so all this data that we're adding to our system we can attach an org ID so that everyone who's part of our organization can edit modify delete Etc if you go to organization settings and Clerk and go to roles you can actually create custom roles over here we're probably going to just keep it simple like member and then if you're a member you can like read and write and delete all these files from the application but there's also permissions as well which I believe you can assign those to roles okay so like these are the permissions over here if you have like fine grain controls you want to add to these different roles you can do that and then also if you click on this organizations link here you will see that we have my awesome team showing up and we have an org ID and there's one member associated with that and if I look at my Cody cybert account I'm pretty sure you can see that they are part of this organization and they are in admin all right so bringing in organizations does make everything a little bit more complex but we're going to get through this if you need to figure out what organization that we're on you can use a use organization hook in your front end and then you get back this data that we can use to basically pass to convex to say you know what give us all of the files that are associated with a particular organization so that's the steps we're going to try to take uh we're going to figure this out so let's go to the UI and let's go to our page and what we're going to do is we're going to go ahead and say const organization is equal to use organization and I'm going to go ahead and Auto Import that if you look at this you get all of this data that you can use now I think what we really care about is organization here so I'm going to go ahead and just say organization and that is going to have organization resource or null or undefined so let's go ahead and just do a DOT on this and figure out what typescript what we have and I think we're looking for like an organization ID so let's go ahead and scroll through this we have an ID here let's conso along this out and let's make sure we're on the right path okay so over here we get the org ID this is what we need to associate on all of our data so that different organizations are kind of isolated from each other okay so let's refactor our convex code a little bit so we can actually store that so I'm going to go to my schema and we're going to store an organization ID I'll just call it org ID um I'm sorry we need to put it on the table so on side of files we're going to say org ID is equal to v. string like that and now if we go back to our mutation it's going to be required so we need to pass an org ID and we're going to assume it's going to come over from args or ID okay going up here we'll say org ID that's also a string and now when we create a file the front end needs to pass in the org ID so down here this needs to be org ID and then we need to get organization. ID okay now there's some issues this could potentially be null or undefined so if that's not defined let's just not do anything when you click the button um and that should be good now the second thing we want to do is inside a get files we only want to get back the files that are associated with you guessed it in or ID so let's just go ahead and bring that in and then instead of doing a full-on query because this is going to fetch back everything from that files table we want to probably create something called an index okay so over here inside of files after Define table we can say index and we could say buy org ID and then we're going to go ahead and and specify What fields we need to index by so I'm going to say or ID and what this allows us to do is now that we have an index we have a really fast way to query for certain things in our database in our case we want to query by or sorry with index by orc ID and then we want to go ahead and query based on now in convex it's kind of a little bit different how you do this query Builder you get this like callback function and you say q and then you basically say return Q do equals or ID ars.org ID and then finally you have to close that and say SCT all right so does that make sense now we didn't pass the org ID when we do the query so we need to do that let's go ahead and we're going to make sure that org ID is defined like this if it's not then we're going to skip right but if it is defined we're going to say organization. ID this is a built-in way in convex to basically say Don't run this query until certain things are done loading often you have to like fetch data from some type of third party service or just fetch data from a query before you can start doing more queries right so there's like a dependency chain in our case we're saying hey we need to make sure that organization ID is defined um like this um I think I'm doing I think this needs to be a um this needs to be an object with org ID typescript is helping me out here okay so recap if this is defined we're going to go ahead and make the query using this object if it's not defined we skip it and so our UI just will wait until everything is ready and then it'll make the actual query so what this allows us to do if um we didn't have an issue with the data so what happens right here is we just change changed our data in convex but we didn't run a migration we didn't Loop over all the records that were already existing and add in an or ID okay so the proper way to do this if you're in production is this would need to be an actual an optional field so V optional okay so I'm going to add V optional there and now our UI won't complain about that it's just now we have a bunch of data that doesn't have an org ID okay so what you really end up doing is you make a file or mutation that Loops through all your data it tacks on a default value and then you can come back through and you can delete this optional field um since we're just doing like early development stages and Rapid prototype we could just clear the table just delete all the documents and we can start fresh okay and what that allows you to do is when you save your file convex can progress forward because it doesn't run into a a collision with how your schema is defined and with the data it's seeing in your table all right so going back to the UI let's just make sure this works I'm going to go ahead and click click me there we go we got two hello worlds now just to kind of verify everything's working I'm going to create another organization okay I'll say other test org I'll create that don't invite anybody and notice that it doesn't have any records okay if I click it it goes ahead and it creates hello world so now I can switch between org one and org 2 so my awesome team and this has completely separate data between my different organizations I also have a personal account that I could use too right which I am kind of curious I guess orgid wouldn't be set for a personal account so that's something else we'll have to kind of consider and think about um CU right now clicking this it's not even doing anything so let's just go ahead and handle that like let's say someone did just want to have a personal account with their own files they want to upload how would we address that well we could basically overload the org ID and probably just store like the token or something um so instead of passing org ID let's let's scroll down a little bit um when we call create file we could pass in organization. ID but this is potentially not even defined right so like if it's a personal account this won't be defined actually let's just console L what this is because I'm really not sure what this is when you're a personal account so let's click it it is null right so we're not part of an organization here we're just a personal account so I think we just need to add a little bit more logic of how this is going to work where basic basically we probably need to also check if this is loading so we're going to get a little bit more complicated here I'm going to say if this is loading then we're going to skip otherwise we're going to use the org ID um if it's defined but in our case organization may never be defined right so we're going to go ahead and also bring in um the user so I'll say use user like this user should have a user. ID on on it okay and so we're just going to go ahead and say like const or ID is equal to organization. ID or we could fall back on user of ID and then we can use that for the personal accounts too and I think that this backward is loaded that's when we should probably say org ID otherwise we should Skip and let's kind of figure out what's going on here so it's saying or is potentially undefined um I think it's CU user could also be undefined all right bear with me we're just going to do a little bit more refactoring um so we're going to call this organization and we're going to actually just pull that out and we're going to say if this is loaded and user is loaded and then we know that we have like some type of org ID right I'm going to try to like figure out a better way to do this for some reason I'm having a hard time figuring this out I'm going to set this to null and then I'm going say if organization is loaded and we're going to go ahead and just like not do turn Aries I'm going to say if this is loaded and also user is loaded and we're going to say org ID is equal to organization dot organization. ID or user. user. ID and then I'm actually going to restructure this a little bit I'm just going to say if or ID is defined then use it otherwise skip all right sorry that was a little bit complex but I think I think I figured out what I need to do and there's probably a way we can clean this up and make it look a nice but let's also we need to make sure we pass in or ID here um which should be defined but I'm also going to say if it's not defined then let's just not do anything um org ID needs to be a string so let's just go ahead and Define that as a string uh and can we set that to undefined or undefined maybe okay I'm just adding types to make this happy because down here it was kind of complaining all right let's just try this out um I know there's probably ways we can make this better so first of all I think you guys get the gist of the sign in sign out stuff I don't think we need this on our page anymore let's just delete it I think it'll make this a lot easier to follow and now if you click on click me it'll create that but it's going to going to use our user ID instead of the org ID okay so now we have like a dual purpose org ID that we're overloading with the user ID maybe it's a bad idea I don't know but it'll work cool so just to verify I can switch between my various accounts and I can have this data basically be isolated from each other but that is only the first part of it right you have to make sure that your backend is properly authenticated right right now we call file and we call get files we pass in whatever org ID we want and all we're checking is to make sure that we're logged in we're not checking to make sure that the user identity that comes back actually has access to the organizations that we're trying to request from right now there's different ways we could go about trying to make sure that the user has access to this or ID one approach is we could make requests to the clerk API and just verify on every single request does this user have access to this or ID that probably won't be the most performant and also there's probably going to be some type of rate limiting that you're going to hit as your application grows larger and you're doing all of these authentication checks all the time another approach is we could actually put a claim on the jdb token to keep track of the org ID that is currently active in the token right but there are some limitations of that as well the way convex works from what I know with this identity token you have to follow properties that live on the open ID spec and so that means we'd have to basically take the org ID and put it into a field that isn't called org ID like gender or put it on a dress or some weird field which feels kind of like a hack so I think the best approach of my opinion is using some type of web hooks so that when organizations are created and also when users are attached to organizations we get a web hook back from Clerk and we can store that in our schema also when users are created we could use that to SC store like a users table so that we can have faster access to like know about this stuff instead of having to do a hack with a token or like contacting clerk every single time now getting that set up is a little bit more involved so I do I'll just copy and paste some code and I'll kind of walk you through how it works so with convex you have the ability to make an HTTP TS file and that gives you a publicly accessible traditional rest API that you can kind of make and have various Services hit right so if I go over here and say http.sys user delated we don't need and basically what happens is you're going to take the request payload and you want to verify that this has been it's sent over from clerk right so there's a cryptographic signature that's attached to it we have to verify that it's all good and if it is good we'll get here and we can actually get the data that came back now the way convex works is that it has different types of runtimes so it has a node.js runtime but it also has a V8 isolate runtime right so this package we're going to end up bringing in called svix this is what we're going to use to kind of sign and verify the web hook that came in but this won't work on a VA isolate right so we have to bring in something called a convex action so that we can actually pass some data and have the action run in node and verify something okay so I'm going to make another file here called clerk I'll just go here I'll say clerk. TS paste it in and here is the package that we actually need to install so I'm going to say imp install svix that should get installed I think we also need to install clerk SDK node I'll just go ahead andall that one some things I want to point out is that we created something called an internal action this is something new basically in convex you have the ability to make all these mutations queries and actions internal which means that they can only ever be called by other queries mutations and actions from in your system okay so they're like private they're locked down there's no public access to them other than if you go to your convex UI you can invoke those internal actions and mutations from your UI if you want to but again why would you use an action an action is used anytime you need to interact with like a third party service in this case we don't really need to interact with a third party service but we do just need to call something from this HTTP endpoint right so now that we have this clerk file we're going to pass in the signatures and the payload that we got back from the web Hook from clerk notice here we're saying context run action and we call that method and then we pass some data and then we will get a web hook result if everything is good and if not you know something will crash and we'll send back a uh an error to Clerk and they can retry later and then over here what we need to do is when a user is created in clerk they're going to send us a message and say hey we just you know someone just signed up in our um off system what do you want to do with it right so we're going to go and go to the schema and I'm going to make a users table so I'm going to say Define table and what we need to store here is the basically the token identifier right so if you go here um I believe this has something called a token identifier on it let's go back to our schema I'm going to put that there I'll say v. string and then we should probably also store the clerk ID that could could be a string as well and then we'll go back to files delete that and when this HTP web hook comes in what we want to do is we want to create a new user so I'm going to go ahead and make a file here called users. TS and we want to make a mutation so I'm say export const create user is equal to a mutation uh like this technically this could be an internal mutation remember we just talked about that because we don't want to call it from anywhere else in our application we want to just call this from our HTTP web hook that comes in okay so what we need to do here is the great user mutation needs to takeen some information such as the clerk ID and um probably also the identifier token you know now I think about it I think we just need a token identifier let's just go ahead and delete clerk user and then we're just going to go ahead and pass in token identifier which we should probably go to create user over here and we should allow that to be passed in as a string like this okay over here let's just do that token identifier I don't know why I had that other thing so token identifier and that needs to take can in our case I believe we have the subject I want to look something up right now we can just go ahead and say result. data. ID that'll be the user ID but I do think there's something else I want to attach to this so that it it'll work fine with our token that we're getting in convex the way I'm going to check that I'm going to go back to our UI here I'm going to click on click me and this is printing out the token identifier notice that my head's blocking it but it's that issuer with a pipe type and then the clerk ID so we can do something very similar when we get that web socket event so I'm going to go ahead and just hardcode this right here and then we're going to pass in the um we'll pass in the ID over here okay so the reason it looks like this is because let's say you have multiple different providers this is a unique identifier knowing that this is coming from clerk versus coming from off zero or some other type of authentication system and so like in our mutations when we're trying to access that token identifier it's going to have this prefix now this prefix probably should come from environment variable when you go to production but it's just going to be hardcoded to match what we have here in the off config for now all right so at this point when a users created in clerk it'll send over that event it should get saved in our database and everything should be good so I think the very last steps that we need to do is we have to go over to Clerk and let me close some of this stuff cuz at this point there's just too many tabs open let's delete that one think that should be good delete that one this one there we go okay so let's go to Clerk and let's do web Hooks and then we're going to create a web hook so that when a user is created um we get that event so let's go to add endpoint and for right now I'm going to add our convex endpoint we go to the docs they tell us what it is so it's your deployment name do convex do site so let's just grab this and this is what we're going to use when we set up our clerk web hook so P that in what is our deployment name it is remember it's in that clerk uh configuration our local so over here it is adventus Cayman 790 paste that in this and then we also need to say slash and then we can say clerk because that is the route that we provided here just path of SL clerk so now that we have the endpoint we can go ahead and filter for certain messages so for example user created we definitely want and then we probably also want organization membership created so I believe this is when a user is attached to an organization I think we also want user updated as well I mean you could add in user deleted and stuff let's just go ahead and create let's look at the event catalog real quick so they kind of list out all the different things let's look at the membership I want to see if that's something that we need so membership created this is saying an organization of this ID was attached to public user uh data user ID okay so I think we we need that what about user updated does this give us the organizations that are part of this user it doesn't seem like it so in fact I think I'm going to simplify this we don't need to listen to all those events we just need to listen to two right now I maybe in the future we need more so we don't want user updated yet so I'll save that one so once you created this web Hut configuration you do need to grab the signing secret and we need to put that in our convex environment okay so that's something that we haven't done yet in convex itself you have to go and you have to go to settings and you have to add an environment variable so here I'll say clerk web hook secret I'm going to paste in that web hook secret okay now that we have that environment variable that's hooked into our Dev convex environment we can use that um somewhere so when we call this fulfill method we're already pulling it in okay so now that's going to hopefully verify that the webook comes in is actually from Clerk and everything thing should be good but at this point Let's test it out I want to see if I were to go to my app and just like first of all I'm going to sign out and then we're going to go to Clerk and go to users okay and I'm just going to go ahead and delete this user which would probably delete his organizations maybe I might just delete the organizations too just to simplify what we're doing okay so now hopefully if I were to go back and log in which I don't even see a login button so I think we need to make sure we had that in let's go to a header where's our login button we don't have one so assign in button and we only want to show that if we're not signed in so I'm going to say signed out there we go they click it that just sign us in and because we're logging in with a email that we've deleted out of clerk that should create a clerk user and then that should send us a web hook event if we go to our logs we should see we get that web hook event great user awesome go to data look at the user table the user was actually not added to the user table so we got to bug that and figure out what went wrong there um because there were no errors in our back end right it said users create user was success but it didn't actually add anything let's go to users real quick and it's because we didn't implement it okay that's why so let's go here I'm going to say await context DB do insert into that users table and we need to pass in the args do token identifier all right the cool thing is I can actually just kind of like fake this real quick if I go to functions I go to users create users um I could just run the function manually okay and I just need to get the token identifier which we should have I think that was like printing out somewhere at some point I think if we just go to like files uh create file oh I have to click the button let's just click the button we got the token identifier I'm just going to go ahead and like force this manually inside a X so that we can store this run the mutation that should create the user go to data go to the users table there it is and now what this allows us to do is we can actually listen from an organizations are created as well and we could potentially attach those to this user okay let's try doing that too I know this is a lot of information but um it's good to understand how all this stuff works because as you integrate with third party services this is real life programming right you have to understand how this works so what we're also going to do is we're going to say here we're going to say orgs or I guess we can call it org IDs which is an array of v. string okay and we're going to default this right now we'll say org IDs is equal to just an Mt array but as you're granted access to more organizations we need to add them to this list okay so let's go to http and we're going to listen for another event type I'm G say case I'll say organization membership created hopefully this would only ever fire after the user event has come in um I don't know if there's a guarantee to that but we're going to make another method called uh we'll call it like add org ID to user and then we're going to go ahead here and we have the token identifier so we're going to keep that but then we're also need the org ID which should be um result. dat dot let's look through here we have organization and then we also have uh ID okay so that's the org ID and then for result data I think this is actually supposed to be like public user data user ID I don't know we'll figure it out so let's go over here I'm going to make another method that is going to be called add or ID to user that's going to be an internal mutation so it's only ever called internally and that needs to take in a token identifier which is a string I'm sorry this needs to take an or ID and what we're going to do is we're going to first going to get back the user information so await context DB dot query we need to get the users table but we're going to have to add a index to schema so I'm going to go ahead and say with or I'm sorry I'm going to say index and then I'm going to go ahead and say buy token identifier and that's going to be that token identifier field and this is going to allow us to do faster lookup so I can say with index by token identifier and we'll say q. equals token identifier r do token identifier okay so look up the user table get the one that's by the token identifier get the user and we can simply just say oride um oh I I forgot to say collect or I think you can just say first there we go and then I can say get the current or IDs um it looks like user could be undefined so I'm going to say if there is no user then we should probably just throw an error new convex error expected user to be defined and then over here we'll just a pin so I'll say user. org IDs and then we'll pass in the new or ID wo okay so that hopefully when I create a new organization that should hopefully append that or ID into my user record now one thing I think we need to go through here is we'll have to edit this and we'll have to say org IDs is equal to empty array um actually that's complaining let's just delete it and then I'm going to save this and I'm going to go back and I'm going to add that there we go okay so now what happens is if I were to go to my UI and I were to create another organization here so I'll say like my awesome theme and create it we should hopefully get that web hook event go ahead and Skip and notice that we got back that record now it did create two records I would expect it to have updated the one that we already have but I think there's a little flaw in our logic let's go back um so we are looking through this users table to get a token that matches a token identifier and let's make sure that these things do they match yeah these things are actually ident identical um here's my issue I didn't update this so we're basically fetching that user record from the database and then we make sure it's there but we need to update it not create another one so instead of just inserting another one we actually should have done a weit context db. patch and then we need the pass it the record here and we need to update orades there you go that's how you kind of do like an update with convex you do a patch you pass it the unique identifier for that convex record and then you update whatever field you want so that should work now if I actually delete this duplicate because we don't want that and we go back to our UI and I create another one called testing go ahead and create it and then go back to our data some point we should get that that websocket event and now I'm part of two organizations okay so let's Circle back why are we doing all this crazy Logic the reason we're doing this is because we want to authenticate the user and verify their authorized to upload a file to this org ID okay in both scenarios right when you create a file and you say okay I want you to create a file in this org ID you want to verify that or ID is in their user okay so I'm going to say const user is equal to a weit context DB Dot and then we're going to say query users and then we want to get with index same thing we did in the other method so let's go to the other method real quick and the cool thing about conf is you can just make these helper functions right so like this whole thing right here I can make it make this into some type of helper function export function hit user and just return that right and this could take in a context and this could take in a token identifier um technically you could just take in the context because what we could do is we can get the identity like this we can do it right in this method and then the identity should have the token identifier and for context we just need to type this and say this is going to be be called a query context or a mutation context those are the types that are built in the convex and now I have this method that I I can actually call anywhere my application actually you know it's probably not a good idea to put that here I'm actually not going to put that there let me let me take back what I just said but we are going to do quer context mutation context but we're going to pass that in as a string here so that we can just like vet so now this is like a private method that we can use and I'm going to go ahead and just say await that context ar. token identifier okay a little bit cleaner then also over here we'll call it again and we can just import it just like a normal function and then we need to get identifier oh I call it identity yeah that's identity. token identifier okay and now that we have the user and it could potentially be null so if for whatever reason the user is not defined we should probably throw a new convex error and it says user should have been defined now the question is do you want to put this in that g user method um maybe we could do that because it looks like it's duplicate over here so let's just copy this I'm going to say constant user is equal to that I'm going to throw an error if it doesn't exist and then I'm going to return it and then we're going to go ahead and just clean this up a little bit too uh same thing over here awesome so all of that so that we can check if user. org IDs do includes ars.org ID so I'm going to say con um I'll say if it doesn't include and the user. token identifier is not equal to the identity token identifier then we can also just throw a new error convex error you do not have access to this org okay so a lot of logic um now what we're going to do is we're going to run this and just make sure this works so I'm going to go over here I'm going to click click me and that worked fine because we have access to to this org I'm also going to try it on my personal account that works fine as well um but for some reason let's say someone wanted the hacker system they want to like manually vote convex we can test that out by going to functions here we're going to go to files we're going to go to create file we're going to try to run it manually and for or ID I'm going to go ahead and just pass in I'll just say testing and then for or ID I'll just say like one two three but we need to pass in the subject and the fake user so the subject is this and then the um issuer is that so if I do this this should throw an error I said that with too much confidence this actually should not have worked I don't think um so let's try to figure out why that allowed us to attach a file to some org ID that I don't belong to I think I'm going to make this a little bit easier I'm going to say has access is equal to user. org IDs includes ars.org idid or user. token identifier is equal to identity. token identifier I think this will be a little bit easier to read and I'm going to say if there is no access I probably could have done some like the Morgan's Boolean logic there um but for some reason it keeps working let's let's go ahead and print out what has access is console.log has access and I want to see why does this keep letting us create it okay it's saying log true so like it thinks we have access to this let's figure out why cuz user. org IDs let's print that out too let's print out user. token identifier and then also print out identity. token identifier oh I think okay this this is the reason why it's has access is true I'm checking literally the same thing um what I need to check is remember the front end could send over the org ID and that could be the actual like user ID I need to say ars.org ID so if the org passed in matches the actual user's token then we have access to that and we could just store it I think that's the issue let's save this and let's see let's run this there we go I think that was our issue let's go back and just delete all these files uh close this delete all those start fres refesh Now personal account all right let's add a couple more console logs um so we can log that out we can also log the user dot token identifier we can log out what org IDs are associated with um and then also log out as access but then also let's just read the code because I think um if the org ID that we're trying to store is part of their array of org IDs or if the user. token identifier is equal to the org ID passed in then we should have access so let's um try again I'll click click me let's see what the org ID is okay I'm also going to set the or ID let's try this I'm going to go ahead and click click me and notice that we got back user for org ID okay I think this is the issue so I just have the user here for the org ID that's passed in from the front end but really what we need to do is I need to say if this includes or.org idid I think that's the issue I'm going to delete my console log so I'm confident that fixes it so yeah again I'm thinking back maybe it's not the best idea to store like the user ID inside of that but it works if I switch over to the actual organization I can create records that are for those organizations so the very last thing that we're going to do with authorization is we have to do something similar for when we do a query right we need to make sure that the org they passing in does the user have access so I'm going to make a function to make this a little bit easier I'm going to go ahead and just say like function asyn or sorry async function as access to org okay and we're going to take this information and I'm just going to abstract that away in return has access and we're going to go ahead and say pass in the token identifier which will be a string and then also pass in the or ID will be a string okay let's do this I think we also do I need the user for anything else I don't think we need the user for anything else so we should also do like a query context or mutation context here and uh let's do this do that user with the token identifier has access to org so what we're going to do is we're going to say con has access access is equal to await context identity. token identifier ar.or ID okay there now the reason I'm not like throwing an error from this method is because I want to reuse this in both a mutation and a query so I'm going to go ahead and copy this code and I'm going to go ahead and just paste it here and I'm also going to say if we don't have access to the files for that or ID so has access just return an empty array and this should also just authorize to make sure I have access or not to the org ID that's passed in okay so now if I go through these everything should still work okay a lot of good work there I'm going to go ahead and commit what we have because we did quite a lot of changes I'll say authorization based on org ID and token identifier add that all in submit it save it all right let's start building out more functionality of this application because right now it's not looking the best so on the page itself let's go to our main page and I think what would be nice is if we had some title at the top of the page I'll go over here I'll say H1 and we can give this a class name of text for Excel um we also give it like a font bold maybe and inside of this we could just go ahead and say your files okay every page should have some type of one and we might Center this over here um so instead of having all this stuff centered like this go ahead and delete all this I'm going to go container MX Auto and that should put it to the left and then we also want to kind of give either some some padding or the margin to the top of this thing so I'll just say padding top 24 and see how this looks um maybe it's a little bit much maybe do 12 and so I think it would be nice to add maybe a button over here where they can click it for uploading a new file to their personal drive or for their organization drive so let's go and in this H1 itself I can go ahead and wrap this in a div and then we are going to go ahead and put a class here I'll say flex and I'll say justify between item Center and that should allow us to put a button over here so I'm going to grab this click me button ahead and put it up there and now they should be spaced left or right okay instead of saying click me we could probably say upload file okay so when we click upload file what we want to do is probably load up a modal where we give them the ability to select the file and then also add a title to the file um so let's go back to the Shad CN docs and we will look for modal I think it's called dialogue dialogue down here there's a dialogue let's go ahead and click it and this is kind of what it's going to look like you click it and we get this nice dialogue where you can have the title of your file or you want to name your file and then also the file itself so let's look at how to do this we have to first install it and by the way I think I'm just going to use light mode for this entire application I've been using dark modes for almost all my tutorials I'm going to switch it up a little bit just keep it at light mode so we're going to install the dialogue component and here is an example of how you can use it you basically just grab this code and you can use it wherever you want so in this case we have an upload button we're going to paste this dialogue code in here and just go ahead and Auto Import some of the stuff now be careful when you Auto Import because there is a radx UI that you might accidentally import you want to make sure you're importing from your components directory okay there we go so now if we go to our application there should be this open button you click it and you get a nice dialogue okay so let's move our button to go and take this button I'm going to cut it I'm going to put it here and I think there might be an as child property we can attach to the trigger and that's kind of needed when you want to Nest a button or something like that cool so we got a a modal going that quick that's pretty awesome now in the model itself we want two inputs so we're probably going have to deal with like adding a form adding some inputs um I think they might give you an example of how to do that so if we go to forms we can go to the form component now Shad CN recommends using react hook form in Zod okay so it's going to take a little bit more effort to get this all set up um but let's go ahead and install the form component over here install that and so they'll walk you through how to kind of create a form validation with Zod so let's just go ahead and like grab this form schema in this Zod component and for right now we're just going to import it at the top of this file it might make more sense to make this be a a separate component um but I think this works for just now uh so in our form schema we want a title for sure and we could just say Min of One Max of I don't know 200 and we probably also at one point want to put a a file but let's just keep going so now we got to define a form so let's go ahead and grab these Imports put them in and then down here you need to define a form that we're going to be using okay so I'll just go and put that there and this will be title we'll default it to an empty string and the way Zod works and react hook forms is you create this hook and you call it use form you pass it your types and then you pass it a resolver and you kind of pass your form schema into the Zod resolver and then you pass some initial values seems a little bit complicated but this is just kind of how it works to have nice type script safety uh with all your forms and now over here we have an on Summit function that we're going to defin so let's go ahead and paste that in as well and then finally let's go ahead and build your form so if we import a bunch of form components down here and also I think we might need to import a input component but let's just paste that and see what happens yeah we also need to add the input so I'll go over here and I'll add from Shad CN an input component okay that's done and then this should resolve in a bit there we go and here is the example form so let's go down and where we want to display that form is in the description here so let's go ahead and paste that entire form there and let's save it and this will be a name of title okay so the reason we're doing all this is so that we have type safety with our form Fields notice how this complained because it knows there's a title to find in our schema if we did something else it wouldn't be there okay so let's change this to title and let's just go ahead and say the title of your file okay now go back to the UI and let's see if we open up this we get a title right here my awesome file we probably want to change this title where it says are you absolutely sure we should probably say upload your file here or something okay so the second input we need would be a input field for a type button so let's just go ahead and copy and paste this and we're going to change this to file which I think it'll airor out for a second but um we'll just change this the file and your file now for the input here let's just say type is equal to file and for placeholder we don't need that all right so now we're going to go up to the schema and then for file we'll say z. any I'm not sure if that needs to be an object or there might be an actual file type let's see Z dot yeah there's not so let's see what happens when we look at the UI here so if we open up the modal we have a title the title of your file and we have these hints I don't think these hints are very useful like it's pretty self-explanatory if you have a a label that says title you know what it is I am going to delete these form descriptions I just don't think we need them there we go and then for the title itself of the modal I'm going to go ahead and just put some padding on that so let's go over here I think we can give this maybe a class name say margin bottom of four just to push it down okay I might even give it an eight there we go all right so now let's test this out if I were to click submit that's going to call this function over here and we should get back some values so we're going to see if file is defined I want to make sure that we're doing this right before we just keep on adding to it so I'm going to click on submit and notice that we do get back a file here now right now it's a string so instead of doing any here actually I'm going to do a uh custom right and so we could just put a file as the custom type and make sure that that is required as well and then down here we can um technically we can make this uh null or file okay and so down here what we're going to do is on the input itself we're going to add an onchange and we're going to manually handle when someone changes this file input right so we can go ahead and do like a call back event here we'll say event and then on the input itself we're going to go up here and we have access to field right so we can say field Dot and we can just call onchange and we can actually pass that event. target. files of zero um like this now this could potentially be undefined so where I'm actually going to do if there is no event. target. files let's just go ahead and return and then I'll grab the first one um so we are getting a little typescript error I think this needs to be onchange we're going to pull that out of this object and then for the other fields we're going to spread them in to um like this so now we can actually just call on change directly think I need to wrap those in Brackets too okay so hopefully this will work a little bit better so now if we go back to our app and just go ahead and type that and I'm going to go ahead and click submit oh I need to select the file so let's just go and select the file there we go so now we're actually printing out the file which is what we're going to need to upload to convexes file storage for the placeholder here I'm actually going to remove that so that we don't have a placeholder and just go and refresh the page and start over Okay so we have validation if I were try to submit this notice that it's going to say you have to add in a file name so I'll say um some txt file and then I'll just go ahe and select the logo and then click submit all right all right I cheated a little bit and I uh Googled a medium post and they gave us a really good solution for basically getting this to work I refactored the code a little bit to get rid of that field that was passed in for render and instead I'm passing a file ref so where this is coming from is I'm basically saying form. register and I'm passing in the name of the form field and then we basically up here I'm changing this to file is a z instance of file list okay and that gets rid of that error in our Y and now we can actually get a list of the files so just a little bit of refactoring but what we're trying to do is when you submit this and the user has a file selected now there's another issue with this um right now it's not like requiring this file I may come back here and yet again we're going to go ahead and change this to a custom uh like this and then we're going to make sure that it at least has one file in that array so let's try it now there we go to select the file that works fine all right so finally what we're trying to do is we want to call an endpoint on comvex to basically store the file that the user has when they submitted so like we have the on submit button here and we have the files that are passed in and so there's some things that we need to do to basically take the file and upload it to convex so first of all let's go to convex and inside of our files inpoint we want to create something called a pre assigned generator or pre-assigned mutation okay so convex has a section that tells you how to upload files to their storage the first thing we need to do is on the back end we need to create a mutation that we can call from the front end to get a generate a upload URL basically so let's paste that in here and we do want to make sure that the user is logged in like we don't want just people uploading whatever files I want to make sure that they are logged in so get the identity and make sure it's defined if it's not we're going to throw an error and then we're going to upload a URL that the user can use to upload a file to convex storage okay so looking back to the front end in the submit function we're going to go ahead and just grab some more code because reading the docs is important we're going to call that method which we'll have to bring in from a mutation over here so let's just go ahead and grab that code and I'll put that up here so we can call it this is api. files. generate upload URL like this and then we're going to grab this function here and that's going to basically call our back in and give us back a URL that we can use to upload the file to and then we're going to grab this we're doing a post request to convex this and in our case the image would be values. file of zero and then we want to go ahead and just get the type from that as well okay and then finally we should get a storage ID so let's go ahead and grab that storage ID from that result that comes back and then we we need to do something to basically persist that okay now before we were kind of creating the file down here when we click this button um that's not what we want to do anymore what we want to do instead is I'm going to cut this out we're going to go up here and we want to basically make sure that an org ID is defined if not we shouldn't be able to submit the form at all and we're going to call create file we're going to pass in the values of title because we have an input box for that pass in the org ID then what we also need to do which we aren't tracking is probably have a file ID we just call it a storage ID for right now and so that means we need to go door back in and we're going to have to modify this to have a file ID now the way you can kind of Define a ID to a storage object is you just go ahead and say ID of storage like this and that's kind of an internal type that convex knows about so now we have the file ID that the user has uploaded and we also need to store that here so I'll just go a and pass it in there but remember we're trying to store a new field that we haven't defined yet so let's go to schema let's go to files and let's go ahead and do a file ID v. ID of storage cool so now that should all work work we might have to go ahead and clear out some of our data um because now we added a required field that's no longer set um but let's go back to data I'm going to go ahead just delete all the files just make sure and go to our app go ahead and click upload file and we're going to go ahead and say testing something out go ahead and choose the file of this logo click submit so that did upload the file but there's two things we need to do first of all we need to dynamically close this model for the user and then the fields themselves we should probably reset so if they were to open this model again those wouldn't be defined so how do we first of all reset the form well I think over here we can just go ahead and say await on this and uh if everything is good we can say form Dot and I think it's reset just try calling that and see if that works if I go ahead and click submit did that reset the form it did now those are both empty so the second thing we want to do is we want to dynamically close this dialogue once everything has been uploaded okay so we could just go ahead and bring in some State I'll say const is file dialogue open and then we'll go ahead and set it as well go ahead and let AI kind of help me out there and then the dialogue itself to know if it's open we'll just go ahead and do this and then we'll also say on open change we're going to pass it that date setter and so this should allow us to basically take this method and we can dynamically hide it if we want to okay so let's try it again I'm going to go here I'll say testing we'll upload a file again and we'll click submit and that automatically closed open it up again and the form has been cleared and everything works as it did before so some things I like to do is I like to show like a toast when stuff happen so that the user gets some type of feedback just closing the modal and having the UI update that could be enough too as well but since we are just trying to learn let's look at more components that Shaden provides us so let's go ahead and find a toast I think it's called so let's go ahead and click toast and this is kind of what it looks like a toast will just pop up when everything has been uh completed so how do you add this well you add this toast component go back to our terminal and import it and while that's importing this is how you can kind of get it set up so go to your layout go to layout. TSX and you can put the toast um pretty much wherever you want first of all you got to import it and then I actually shouldn't be importing in there I'll say Toaster okay so now that it's set inside of the toaster here like they told us to we should be able to bring in this toast function after calling this used toast method so let's go back to our page and we are going to go ahead and just import that toast function from Ed toast and then over here if everything was good we'll just go ahead and call it I'll say variant is going to be uh the default and then you can pass a title in a description here so I'll just go ahead and pass a title that says file uploaded and then for description I'll say like now everyone and view your file all right let's try it again so let's open up this I'll say testing we're going to go ahead and get the logo click submit and the alert did not pop up not sure why let's just do a hard refresh sometimes you do have to refresh your application all right there you go the toast did show up at the very bottom there it says file uploaded now I would kind of like to make this green instead of white so we could try to dive into customizing Shad scen a little bit to give it a different variant so let's open up the toast component okay I'll go to the components directory so if you look at this toast component there is a toast variance variable and we can just go ahead and say like success and we can Define this however we want so we can go ahead and say BG is um success maybe like that now I don't think this is a color that we have have defined so we may have to go to our like CSS or our tail one CSS file and we'll have to go down to our colors and we're just going to add one here we'll call it success and then we're going to copy this and then for default we do need to go to the global CSS and we need to find a success basically we're going to stick with light mode so over here we'll just say like um sess is going to be equal to hopefully we can get like a green all right so let's go find a green hsl like let's just this minol one looks pretty good I guess and we're going to go ahead and just put that there just delete some of the stuff and then we'll do the same thing for the foreground so I'll just go ahead and say paste this this will be the success foreground and we'll just pick a uh maybe this this white or this black so let's just go ahead and paste this in as well all right so what we're doing is we're defining custom colors this is just a normal CSS file that has CSS variables in the tail one config we can actually import those colors and then in our code we can start using BG success or we can do success text success uh foreground I believe okay so now that we Define a new success variant we can go back to our page and we can find where we're doing that toast which was down here and now we should be able to have a success variant and hopefully if I did this correctly we should see a green alert pop up instead of that white one there you go file uploaded now everyone can be your file so that's basically how you can customize Shad Cen with different colors or different variants just have have to kind of Define your colors in the global CSS and then modify your colors inside your Tailwind config and then you have access to them inside of your components which you could actually use here but if you want to be lazy technically you could just say BG green or 100 if you wanted to and you didn't have to do all this extra work but we're trying to do stuff correctly so there's a lot more stuff that we could do for example when we try to create the file if for some reason this request failed we could probably catch an error and then we could also show a toast for a destructive saying hey something went wrong right so let's just go ahead and put all that code inside the TR catch and then inside the error we'll put a toast here and this will be a variant of destructive and we'll say something went wrong your file could not be uploaded try again later now would this actually happen on the backend I don't know but we could test it out and so where we're calling this function the crate file we could just go ahead and say like row new error you have no access and if I save this um I have to com I have to comment all this stuff out I just want to kind of demo that all right so let's try this out now that we have a destructive thing I'm going to go ahead and just go here select the logo click submit and now we get an alert that says hey something went wrong um I do want to fix an issue when you close this modal and come back it has your old value persisted probably not the best user experience we should probably clear that out um so let's go ahead and do that so somewhere when we close this model which I think is over here what we actually want to do um I'll just call this is open we're going to call the method here and we're going to say is open which will just make it set to true or false depending on what action happened with the component library but then we want to say form. reset so that we kind of clear it out for when they open it again let's try this I'm going to close it open it back and now that's been cleared out let's test out the file as well and I'll close it come back and it's cleared out but wait there is more so you might be asking why are we spending so much time with the modal I'm trying to make this as polished as possible maybe we can kind of go faster and move on to other things but when you click submit there is a delay so it' be nice to disable this button and probably show like a loader or a spinner inside of it so that when we submit like the user gets some feedback that something's happening and so the way to do that is on the form itself there's like a form State object and we really just want to go to the button um for where we submit this so I think that's down here there's a button we should probably disable it I'll say disabled is equal to form. form state do is loading if this thing is currently submitting we do not want to allow people to click it again let's do that and then um let's go back to our files here I'm going to go ahead and just undo all that stuff we kind of hacked in to test out error States let's go back to the page and um we should be good so let's go ahead and just click it um for the success alert that should probably be darker so let's go back I think for Success foreground we actually want to do like a darker color so let's just do this black this hopefully that looks better there you go all right so in terms of getting a spinner in this button so like when they actually submit it what we could do is we could say form. form state is loading and then if it is loading we're just going to show a loader here so let's just do that now I do think Lucid icons are already kind of baked in by default so we can do like a loader two and that should show a spinning icon for us now we might also need to add like some class name here and say Flex gap of two just so there's space between the spinner and the text um but let's save this and see what happens actually what you can do you can comment this out and say disabled true and that'll kind of make your feedback loop a little bit faster over here we could do it as well so true that's kind of what it's going to look like um The Gap is a little bit big so maybe I'll I'll do one there and probably get rid of the margin right okay cool so now go back and set it to the real bullan State and we should be able to submit this and it didn't show the spinner I think what's happening is I shouldn't be using is loading I should be using is submitting let's try that again there we go cool so now the form and the modal it's all looking good I think this is polished enough or we can move on to some other stuff I know we spent like a long time doing that but it's nice to polish your app up and make it nice and usable so let's just go ahead and commit everything we have here go a and say add and I'll say adding the upload file uh dialogue and I'll just go a and commit that and sync that up all right so let's move on to styling this homepage because right now it just doesn't look that great um I think there's two approaches that we could use to show your files obviously a table or you could do a collection of cards they both work well and maybe we'll do a toggle so a user themselves can pick what view they want now before we dive into to styling the page with like a table or a card I do want to do a little bit of code refactoring because this file is getting a little bit out of hand I mean it doesn't seem that large but there are better ways we could styl this or structure this so I'm going to go ahead and make a new file here called upload button and we're going to call it TSX and what we're going to do is I'm literally going to copy everything I'm just going to put it in that file and then we're going to delete the stuff that we don't need so for example we just want to keep the dialogue that's all we want to keep we're going to kind of abstract that away into its own component then everything else uh we don't need anymore so we'll do that and then what we're going to do is we'll just look and make sure if there's anything that's Gray from es lent just delete it okay so we need to keep all all this on submit stuff we got to keep the form stuff um go ahead and delete that delete that there we go and so now we go back and find where we export this instead of exporting home I'm going to export a function called upload button and now our page doesn't need to know about almost all of that code right we could just import an upload button and then I can delete the entire dialogue here save that and then let's go through this code and just delete anything that's no longer used so on submit no longer used file ref no longer used form is gone that's gone that's gone uh create file is gone that's gone gemma's gone and notice that we are slowly making this file so much more less and it's going to be much better okay so let's just delete all this stuff there we go so our page is nice and clean now it's nice and lean and all of the stuff related to uploading a file is encapsulated in that so I can just go ahead and do something like this and grab this file upload it there we go so remember to refactor code along the way so let's move on to this page this page doesn't look that great we do have our files displayed in this ugly list and what I want to do is I want to bring in another component card uh called the card component and uh this is kind of like what a card looks like let's just install it so go to the terminal go here paste that in okay and then here is an example of how you can use it so you basically import all this stuff and technically we could make another component too if we wanted to I could make one here and I can call it um file card. TSX export function file card and we're going to import those things and then we're going to go to the example and make sure we just returned this okay so now we should be able to go to our page and instead of rendering out these divs we can render out a file card and import it we have to figure out why this is complaining it's saying that children is not defined okay so let's do this now what we need to pass into this thing is probably the file let's just do file there and so if we go to the file card component we probably want to Define um some props that takes in a file and file is actually coming from convec so if you actually import this doc type you can get the exact type that's stored in our table like this so go ahead and bring in that type and now file should have for example the title on it so if I were to go ahead and interpolate we should have the name um in our case I don't think we have a description um so we'll just comment that out for right now we have the content here you can put whatever you want we have a footer which is in our case we should probably do um a button that maybe opens the file or downloads the file or something I'm not really sure depends on what it is if it's an image maybe we should open it in a new tab um but we'll just call it download for right now and uh for the content itself we probably need to put like um a type or an icon but let's just see how this looks like if we go back to our UI this is the cards yeah so it's in a list right now not the best we should probably have this in a grid so how do you do a grid well if we go back to the page basically we could wrap this whole thing in a grid like this and then we could say grid calls 4 and then we could put some Gap in between these things okay so now we have all of our files showing up there and then also I'm going to put like margin bottom of eight on that just to kind of push it down so now that we have all the cards displayed I think there's a couple things we could do first of all the ability to delete a file probably is the most important thing we should add in so let's see if we can do that um and we'll come back and like get some better data displayed on these cards so in Chad CN there is a way to have a drop down let's go to drop down menu and this is what it looks like okay and so what we could do is import this drop down component go to my terminal install it and while that's installing let's just go ahead and make another file here and technically this could be part of the file card like we don't have to make another file we could just put it here I'll say function and this will be like file card actions okay and then we're going to go ahead and just import those drop down menu stuff and then we're going to grab all of this and return it okay so now that we have the file card actions the thing that we care about is a delete let's just go ahead and put delete here and everything else will go away and then we should be able to have a delete icon like this and on the label again we should probably say Flex gap of one or something to space that out and so let's take this and let's display it um in the title itself so inside the header uh maybe we can put it here we have to figure out where the best place to put that is so if we go back to our app and um we we do see open here so if you click open notice that you get a drop down awesome so let's do a couple things first of all this whole thing needs to be probably text red of let's just do like uh 600 there you go and then also probably items Center so that it's vertically aligned there you go and then the icon itself um this delete icon honestly that does not look like a delete icon rather have like a trash icon and then it's a little large so I think we can actually size this by saying class name we can say width of eight height of eight that should kind of size it down oh that actually made it too big let's go make it a four maybe there you go now they're using a menu item I think I should probably use an item not a label so let's go ahead and use a menu item and go back and see to be at hover State here it looks pretty good uh some more things we should probably do is I'm going to say cursor is a pointer it's kind of obvious that it's clickable and you can kind of click this on any of these now I don't like how it says open here I rather it be an icon so let's go to lucid dodev and I can go ahead and search for like dot we want to look for three dots U maybe menu here we go let's grab more vertical that's what we're going to use so instead of having open here just say more vertical and then going back to application now we have the more vertical popping up everywhere I think it would be better if it was like positioned over here maybe like with absolute positioning so on the header itself I'm going to give it a relative and then I'm going to move the file card actions out I'm going to wrap this in a absolute positioning like this save that and then I'm going to say top of one right of one and let's see where this puts this okay so there it's over there I might move it in just a little bit top of two right at two so now when a user clicks this delete button we should probably show a confirmation dialogue typically when you do destru destructive actions in your system you want to give the user one less chance to like are you sure you really want to do this and if you go to Shad CN there is something called an alert dialogue that looks like this um and that's what we're going to bring in so let's go ahead and import that the new component we haven't used yet so I'll import that in and the way this works let's just go ahead and import this whole thing I'll put it right here and then we're going to grab this whole alert dialogue here and we're going to paste it next to the drop down menu so every drop down menu will have their own dialogue technically we could also put this at a higher level if you want to um but for right now I'm just going to make this easy on myself and just put this next to this menu button because we want to basically open this based on state so I'm going to say const is confirm open and go ahead and import you state there and here we can say open is equal to is confirm open and then on change we're going to go ahead and just do that what is keep the text for now um but what we want to do is if they cancel it I think this will automatically close and if they click continue that's when we want to call like a convex endpoint to actually delete so I'll say to-do actually delete the file so let's test this out I think in order to get this showing up on the menu item itself we'll have to do an onclick and we'll have to open it when you click it so let's go to our UI let's click it and we see the dialogue are you absolutely sure make sure that you can close out of it and I'll click continue and that works too so now we have some type of confirmation dialogue that's good we just need it to call convex now so if we were to create a convex endpoint inside of that files file let's just make a new mutation I'll say export const delete file equals a mutation and the thing that we're going to have to do here is take in a file ID I I say b.ed do files and then we first of all want to make sure that the user has access to the org so I'm going to copy some of this code like that and for the org we don't know what the org is right so what we need to do first of all is if the user is not logged in I mean this is a mutation so we should probably like throw an exception just do that uh B that there I'm sorry paste that over here and then we want to get the file so I don't think we've done this in convex yet but you can get a file back by saying await context DB get and then you pass it your file ID so say args file ID like that um args with an S so they will get us back the file if it exists now if it doesn't exist we should probably throw an error saying this file does not exist but if the file does exist we need to do that whole authorization check do you have access to the org ID that the file is associated with right so I'm going go ahead and just say instead of this org. org ID we'll say file dot or ID okay now again if we don't have access we should probably throw yet again another error you do not have access to delete this file file there you go and then finally what do we need to do well we have to delete the file from the system there we go a lot of stuff so let's make sure this works um we're going to go ahead go back to the file card and we need to import that so I'm going to say const delete file is equal to use mutation I'll say API files delete file like this and now we need to call it make sure we Auto Import that but we're going to call that mutation after they've clicked confirm so I'm going to go ahead and call this here and we need to pass in the file ID now we don't have the file because we weren't passing it in here but for right now what we could do is we could just say that just have that file passed in as props which means that we need to go down here and we need to also pass it in as props awesome so let's click it and delete are you sure yes and this through an exception um it says the argument to a convex function must be an object so I think I'm actually calling this incorrectly um this needs to be a file ID like this and I think that should work and the reason I need get a typescript error is because for some reason I did not Auto Import this thing so I need to do that there you go that would have errored on us um if I didn't if I had that imported let's try it again delete this and I'll click continue and now it's gone and again it's good to have maybe a toast pop up after you do something in your system so let's try to do that let's go back to the upload button let's bring in the toast so let's go up here I'll go ahead and just add the toast this component like this and then how do we use it well let's go down to here where we call it and we want to go ahead and just after this thing is successfully deleted so I'm going to wait on it I'm going to go ahead and just show file deleted your file is now gone from the system okay let's try it again I'm going to go ahead and click delete click confirm and it says file deleted now I don't like the the green success for like deleting uh I might just do a default instead I think that's a better color so I'm going to do it again and there we go just go ahead and kind of clear up some of the stuff delete as much as we can all right so let's just commit I'm going to say add ability to delete files and I'll go ahead and commit that and sync it up think we're making some good progress now um again let's just go back and add a little bit of Polish cuz right now if you were to delete this last file this UI looks kind of plain right it looks bare what we could do is we could have some type of placeholder State saying hey there's no files go ahead and upload one so how do we do that well the first thing I'm going to do is I'm going to find a placeholder image one site I really like is undraw they have like I think these are free to use like open source illustrat ations they might be Creative Commons I don't know if you have to like uh give attribution or not but let's just go ahead and go here and then we're going to make it black and white because that kind of meets the theme of shad xen and then we're going to go ahead and search for let's just kind of look through these and see if there's something here like empty maybe now I don't know what the best image is honestly I'm not a designer but this image folder one kind of looks like it might be interesting so let's download the SVG and we're going to copy this into our project so let's go ahead and open it up and we'll drop it into our public folder here and I'll call this mt.svg when there's no data here we want to show that image so let's go back to our page and the way we can do this is first of all let's just grab in this image I'll say image and that's going to be a next image component and we're going to say alt is um an image of a picture and directory icon and then the width will be 200 height will be 200 and the source is going to be slash empty.svg I think that's how you do it so let's go back to the UI does that show up and there you have it a little bit small maybe make it a little bit bigger but the thing we want to do is we only want to show this if files is done loading and there's no file in the system okay so the way convex works is that this will be undefined if it's loading but if it is loaded you'll have an empty array so I'm going to say if files. length is equal to zero then we can just go ahead and display this um but yeah we also want to check if it's defined so I'll say if files and files. length is equal to zero we'll display that image you have no files go ahead and upload one now all right so we'll put this in a div I guess and we'll wrap that and then we're going to go ahead and put a fragment here actually no we'll put a div here we're just going to wrap everything in divs and so what we could do here is we want to give this a flex column so that the text is directly underneath that image okay you have no files upload one now it simple and then this will be uh we'll give this a class name of text to Excel and I think right now it' be cool if we maybe like Center this on the page now right now this is kind of this UI is a little bit bare right so I think we should probably if there's no files we should probably put this centered on the page and then when there is files we can kind of move this back um to the left so let's go here and I think we can go ahead and just say MX Auto to Center this um we might need to say actually no I think we could say like w full let's go ahead and hover over this and see okay so this is in columns that's what's going on so I need to actually get this out of the columns and put it up here now for down here let's save this and let's make sure this is working and why is this not centered because we didn't add items Center that should Center it margin top of 12 we'll push it down a little bit and then this what we could actually do is just hide this entire header and only show it if there's files so I might actually cut this out and I'm going to say if files is defined and files. length is greater than zero then we're going to go ahead and just show the header and we're also going to show this div there we go all right so you have no files upload one now and what we could do is actually just put an upload button down here and now since the beauty that we abstracted that it all just worked Works without having to do much code at all um let's put some gap between this stuff so let's go up here I I'll do gap of eight instead that looks pretty good and um yeah so now after we upload a file that should switch it back to the header upload button up here and then also I think I need more margin there I just do 24 and let's try it I'm going to go ahead and upload a file testing I'll select an image click submit and there you have it it looks pretty nice and now if you were to delete it it'll go back to that you have no files State there you go now there's one thing that we need to kind of work on when the page first loads it's just blank there's no spinner there's no loader and we could do a better job at improving that a user experience I think so what we could do is up here I'm going to say if files is equal to undefined then we could show a spinner now how do you display a spinner well we did it in the button already right so if you go to the upload button there was a loader let's just go ahead and grab that and just place it right here and we can make it big we can make it 24 and uh we can just go ahead and say if true or this just to kind of see what the spinner should look like um let me just force that to be true so that's what the spinner looks like I maybe even make it a little bit bigger so like this could be 30 is that a is that an actual thing 32 let's just through the 32 here there you go underneath it we could say yeah let's just put loading there we go um yeah does that look good we just have the center on the page and then again how do we Center it well we could probably just grab actually all this just grab that whole give and that should make it look nice and I could say loading your images now I think I could say text Gray of like 700 I feel like the black is a little bit intense I think the gray of like 500 or 700 might make it less there you go that is inyour face right okay so now we just need to make sure that um we only show it when files is equal to undefined if I refresh the page just see loading your images you show you show this and then we're going to say testing here and then we are going to do the screenshot all right go ahead and save that again now having the full page have a spinner probably also not the best user experience we can come back later and do server side rendering if you want to prefetch some of this data so that it doesn't even need to show a full page spinner you can do that um I just feel like just showing a spinner while stuff is loading is actually a little bit easier but we're also going to going to say const is loading is equal to files equal to undefined okay I want to kind of make this code a little bit cleaner because right now just checking stuff like that not the best um and then over here if it's not loading we could do that so in the card itself we have different files that users can upload we have like images we have csvs PDFs and I think we should track the type of file that a user is uploading that we can actually display different images okay right now we don't track that so I think we should probably try to figure out a way to track that let's go to our schema and on the files I'm going to go ahead and say type now one thing we haven't done yet in convex is you can actually do a union and you can say v. literal and I can say this is a image um just say like image um it could be a v. literal of a CSV rv. literal of a PDF so those are the the three things that we're going to support for right now and what we can do on the front end is when you do the upload button and you try to upload to the back end we need to make sure that the back end gets a type okay so when we call create file here we should probably Tak in a type so I'll say type and so for the type we actually need to Define that exact same Union so one thing you can do in convex is you can extract this to be a function or a variable right so I can say const file types is equal to this and we can say use file types here make sure you export it and then you can actually import it over here I can say file types so that we have the shared type validation rules between our schema and our mutation when we pass in data all right so down here we need to make sure we pass in type and we'll say ar. type and now in our front end we just need to make sure we pass that in so somewhere which is here we can say type is equal to and I think we have access to the file right so I can say values um values. file of zero Dot type now unfortunately type is going to be like uh I don't know it' be like application PDF or text PDF or something I have to go look at the header mean types so we can actually do this correctly for right now let's just do image but I am going to console log this like what do the file types kind of look like let's go back and we're going to upload a file we're going to select a PNG I'm going to click submit and we got image/png for a PNG for PDF it is application PDF and for a CSV it is textcsv okay so let's just go ahead and I guess do the conversion in the front end I guess say cons types is equal to a map of image PNG and that'll be an image we could do application PDF and that'll just be a PDF and then we could say uh text SL CSV and that is going to be a CSV and then what we could do is we can actually give this a type as a record which will be a string and that'll map to a doc files type I do this correctly and so now now we could just basically pass in types of values file of zero type now I don't like how we're kind of doing this in two places so I'm going to goe and say like const file type is equal to this and then we're going to go ahead and just pass in file type uh here again we'll do file type yeah so this should hopefully work let's try it we're going to go ahead and just start fresh go to our console here here and clear that out let's upload a new file I'll say testing stuff out we'll select the file we'll do a PNG first and I'll submit it and it does throw an error it says object contains extra field type that is not in the validator okay so I think what's happening is that the convex Runner is actually not working um because I need to go and probably delete some data here so let's just go and delete that one file we have because it doesn't have a type then that should hopefully rerun convex in compiler code so now if we were to upload a file here and say testing give it a PNG submit it and it's happy now let's do the same thing for my PDF subit that and then we'll do my CSV okay so looking at the data we got type of CSP PDF and image so that's all working correctly now we can use those types to display different icons here so instead of saying card content which isn't very useful we can instead uh figure out where those cards are find out where we're doing card content and instead we will say file. type and we let's just print it out and see what it is okay so we have PDF USV and an image and instead of just saying like the types here let's see if we can do some icons so we're going to do a similar lookup map like we did on the upload button so just go ahead and grab this and I'll say like type icons and this instead of saying string to this convex type we're going to go ahead and just say react node which allows us actually I need to import this this allows us to actually do some icons here so I can say like image icon uh this could be like a PDF icon if there is one there's not so let's go to Lucid let's see do they have PDF they have file text they have CSV so to keep this simple I really don't want to pull in yet another icon Library let's just kind of get it as close as possible so we're going to go ahead for CSV we'll put this one and then here we'll we say like text um file text icon let's do that and at some point we can come back and actually find real like PDF icons or something why is this complaining cannot find okay there we go so now what we do is we can just interpolate based on the type that we have and instead of image PNG PDF we'll do this and we'll kind of reverse the order so this is going to be Doc and this will be images and this will be type oh sorry files not images my bad cool so now we should see hopefully in our UI we should see different icons for what these are we could probably Center them um so over here text content we'll say text of Center hopefully that'll Center it does not uh maybe I need to put it on the P itself text Center and delete it off of this still not working okay we'll get there um let's make it a div and then let's give it a flex and I'll say adjustify Center cool and now instead of putting the icon here we should probably put it to the left of the title let's actually take this and we will put it right here in the title and then on the title itself we'll say class name flex and then that will be uh yeah let's just do flex and see what happens okay now let's do gap of two there you go so for certain files for example if it's like an image we could probably do a preview here for other files I'm not too sure for a PDF in CSV like you'd probably have to add some complex backend logic to like take a snapshot of the first page and then like put it here uh same with the CSV so I probably won't do that in this video but at the very least if it's an image we could display that right so let's go and in the content let's just do a a thing here and I'll say if the file. type is equal to image we want to show a react image so let's just do react image like this and we'll just go and say like is the file name width could be 100% height is 100% source is that you know this is do width of like 200 height of 100 for now okay so we don't have a URL so what we're going to do is we're going to make a utility function which knows how to look up where our URL and our images are stored in convex so over here I'll make a function I'll say function get file your URL that'll take in a file ID and we could say Doc uh yeah files of ID and then we want to basically return some type of URL which we don't know what the URL is so this would have to be I think it's already in our next next public clerk URL I think we can just grab this I'll just do that and and I think I have to go look at the clerk docs I'm sorry I need to go look at the convex docs which are here and then I'll say file storage upload serve let's look at here what is the location of all these things I think they tell you where it is um so they do kind of tell you where the Ural is here if we actually go to our files and we copy or click download um actually I'm going to click on copy link address I do believe this will tell us the location too so it looks like this it's the convex URL here convex Cloud API storage and this actually will be the file ID cool so in order for this to work in nextjs we have to go to our next config and we have to include that as a an allowed like domain I forget how to do that I think you do images you know instead of guessing let's just go to the next docs and figure that out all right so here's the example you actually have to just like add this remote patterns type of thing um I think all you really need is a host name so I think you do images that's an object that has remote patterns which is an an array that has an object that has poost name we can actually paste in that convex URL so then nextjs will allow us to view images from that URL okay so using this helper function we're going to go down here and we're going to just call it with file. file ID and um idea files I think this actually needs to be ID of storage like this there we go so now if we go back to our UI hopefully um uh if I just do a hard refresh maybe that'll fix this error so now the image shows up inside of our cards here now I do think um four cards might be a little bit too much I might go to the page and find out where we're doing the grid I might make it a three I think three might make more sense um maybe you have more real estate with wise you could add more stuff um and for the PDF and the my CSV it looks kind of weird that there's no like image here so I might make the same icon that we're doing here and just show it full so let's go to where we're doing the cards here file card and then we're going to say if file. type is equal to dsv we can go ahead and just show a what did I call it up here I could just do file types like this and then we could bring in CSV but unfortunately I think I'm actually I might just have to do this I'll just do CSV and I'll render it like this so I can give it a class class name width of 20 height of 20 kind of see how this looks okay do the same thing with PDF and we'll use whatever the PDF was file text icon like that there you go and you should probably also give it some type of uh overall width or height I think we can give the card content maybe we can just give this like a fixed height of 200 pixels and just see what that looks like okay and then the content of it we can just go ahead and say like Flex justify center items Center that should Center the images on the page okay looks a little bit better it's not perfect and then the download button but um I feel like the download button should probably be centered too so this over here will say class Flex justify Center all right so now if we were to click on download how would we get this to actually open up a new tab and download the file okay so unclicking on this let's just say unclick we're going to go ahead and open a new tab to the file location on Con X so I think it's as simple as that now sometimes this doesn't work on certain browsers so like you might have to go and double check that but let's click it and now we get the image that pops up over there cck on the PDF it shows up inside a previewer click on the CSV and that downloads awesome so let's go ahead and commit these changes I'll say showing image preview in car cards and ability to download the files all right so let's just keep adding stuff to this I think one thing that every type of upload system might need is a filter or a search bar so I think we're going to focus on that and maybe we can search over the title of the file let's go to our page and we need to add basically a search bar component so let's find the title here I think there's an H1 on this page somewhere here it is and then under this let's just pretend like we have a search bar and we are going to go over here and just make a new app search bar. TSX and we're going to say export function search bar and what does this thing need to have it probably needs a form and it needs an input box and it needs like a search button since we know we need a form on this page we might as well just go to the page that has a form already which I believe is our upload button let's find a form and we're probably going to grab a lot of this same code and it's going to be pretty simple we just need like a form schema for keeping track of the search criteria I'll paste that in and really all we want is the query I guess I could name it query all right what else do we need let's go here and we are going to construct the form let's put that at the top of the component and import some of these things make this an empty string as default then let's continue down so for the form itself we are going to need an on Sumit button so let's just go ahead and grab this on Sumit function like that and then we also want to find the form component which I believe is down here near the bottom here it is let's just grab all the stuff in the form and then we'll delete stuff that we don't need okay we'll import some stuff along the way add in some form Fields form items form controls and we'll call this query and let's keep going down so we don't need a file anymore so we'll delete that we do want a search button and we should probably put an icon in here this says like search icon all right let's see how this looks like and we'll say this is going to say search also the loader okay so now we got a form that should show up in our UI somewhere if we're displaying it um let's go over here I don't think I'm importing the search bar so let's import it and refresh the page there you go it says title and we have a search button now if I click it it'll say You must contain at least one character now technically I don't think it needs a character like if you were to delete all these and click search it should show you everything um so let's change that a little bit I'm just going to go ahead and find in the search bar Min could just be zero that's fine so now it won't give us an error if you don't search anything and I do wonder if maybe it'll make more sense to put it right here so that we have more like horizontal real estate so maybe we could actually put the search bar inside of that header I'm going to cut it out I'm going to scroll up here and we're going to find um the header which is here paste that in there you go we got the search bar um let's go ahead and Di this a little bit so the first thing we need to do is on the form itself we should probably say Flex gap of four that should put it on the same line and then additionally we should probably say um actually I'm going to do gap of two then I'll say item center now this label right here is actually messing up the search so I'm actually going to get rid of the label uh I'll just get rid of that label for right now now we have a nice search area and then on the input itself we probably need a placeholder so I'll say placeholder is your file names there okay so now the idea is if you were to search we need to basically change the convex query that we're doing to only give us back items that are related to the text that we're searching over um so let's go ahead and try doing that now in order to do that I think we're going to have to elevate State we're have to lift State up here so I'm going to say const query and set query is equal to use State we'll set it to empty string for right now and for the query text we're going to change this function a little bit so the first thing I'm going to do is we need to update the convex get files function to accept a query so they can pass in whatever string they want and it's actually optional okay so it's optional they don't have to pass it in now there's ways we could actually do like a a search index on convex um we might do that in a little bit but the easiest approach if you don't have a lot of files like for example if your or only has 300 or 400 files it's not slow to just filter in the JavaScript right all of these m ations and queries run on the database that convex runs on their services so unless you're dealing with like a lot of data it's fine to just get all the files like this and then you could simply just say files. filter and then we want to get back the file whose title or I guess you call it name includes uh the query that was passed in from args okay so just do a JavaScript query and that works for a majority of cases now if query is not defined we probably shouldn't do a filter right so I'm going to say if args do query if it's not defined just return files also if it just equals to like an empty string um oh that that'll actually do the same thing I don't need to do that but if it is defined let's do that and that should hopefully um we're actually running into a little bit of issues so I think the typescript thinks that this could potentially be undefined even though I'm checking to make sure it's defined I wonder if I actually swap these would that make it happy um no let's do this I'll say cons query is equal to ar. query and then I think this will make it happier sometimes you got to play around typescript to make it happy um and technically you can do a turn a if you want to like Cory toine do the filter otherwise don't but this should work right so now if we go back to our front end we can pass in the query which is going to be in state here all right and then depending on the query we'll have to pass this in to uh that search bar right so I'm going to go ahead and say set query we're going to pass that in we also probably need to pass in query here so that we can actually like populate the form with it let's go over here and we're going to add query and set query query is a string set query dispatch um I forget what the type is let's go back to the page and just kind of look over what that is defined as the dispatch that state string okay so let's just go ahead and do that all right so now this search bar component has the ability to modify some higher level State and basically when we submit all we need to do is just say set query equal to values. query actually let's not pass value in let's just do this click testing click submit and um figure out why this failed okay so I think there's a little issue with you search for something you got no files back but our page thinks that we're in like the empty State placeholder so we have to kind of modify that just a little bit um let's go over here and we only want to show the empty state if query is equal to um nothing right so if query is not defined then that's when we can show that we probably want to actually show this thing so we're just going to do some refactoring because now our business use case has changed a little bit and we can actually probably copy this whole thing just copy this whole logic here and I'm going to put it here and then we don't need to check if it's loading but we do need to check if files is equal to nothing we should probably display that okay so now I can type in like testing I can type in my and delete it and clear it out awesome and then also if I were to like delete all my items I think the best way to check that is just to switch to a different org and go ahead and try that so there's a little bug here where basically the placeholder state I'm actually going to abstract this away to a different um component up here I'll just go up here and say function placeholder State I just call it placeholder and then we're going to return that because we're going to use this in different places so first of all I'm going to put it here and delete that and then up here we can make this a little bit cleaner too and now let's think about this logic so we are loading the page and we don't have any files um but query is also not defined you know now I think about it we should probably have this header show at all times I think I'm over complicating this I think we could just do this and then the header will always be there your files you can search for files upload a file you have no files and then if I were to switch back to a personal account we get some files there I think that's a better UI and a better user experience instead of having like all these different views so yeah this looks pretty good in my opinion now one thing I noticed is that if you were to do like my CSV notice that the results don't come back so we should probably go back to the back end where we do that filter and then we should probably say name to lowercase then also the query that we send in we should to lowercase that as well just so that we can match based on like criteria um that match but aren't case sensitive there we go that looks good testing testing now I don't like how big this button is so let's go back and find that search bar and we're going to find the button which was down here and I think there's a size I think you can say size and and we can give it a small just so it's not as large I think it's kind of distracting from the page now people can type in like whatever in search well so some additional features we should probably start adding is maybe a side navigation because adding the ability to like favorite or Star different documents might be important so for adding a side naav let's just add it to the page right now but in nextjs you can use layouts and so if we wanted to do nested layouts and have like the URL match what side navigation link we've clicked we can do that too but I like to build stuff as simple as possible and then refactor later um so for the side navigation we could just make a div here for right now and I'll say testing I want to make sure this thing shows up okay and then we want to split it left and right so there's different ways you can do this um one way is you could potentially just make an entire wrapper here um and we could say flex and then we're going to wrap these two components uh inside some so let's just go ahead and say like div and we'll do that okay so now we should have testing on the left and our content on the right and really what we want to do is probably just give this some fixed width so I'll say like width of I know 32 see if that's large enough maybe a little bit bigger 40 that seems like it's pretty good and then also notice how there's like some space here on the side that we could potentially expand so I think here we could say class name of w fo which should expand it to the full page Okay so we've lost some horizontal real estate but again we're trying to add a side navigation here and so some of the things we could add in is maybe a link for viewing all your files so we're going to go ahead and just add like a um a button for right now I'm going to do buttons but we might come back and add them to be links let's import the button and I'll say uh files and this could be a file icon like this okay and then you can actually do a variant here so I can say like link you go maybe view view files might be a better name for it or all files um on the button itself let's give it a class name of flex and gap of two so that the icon is spaced out a little bit and I still don't like the spacing of this so let's go ahead and look here um right now I think it's centered aligned I think I'd rather like it if the button was left aligned so I'm going to say flex and then justify start I think is what we want um I do want to add some more space between here so we can do a gap so let's go we're doing flex and I'll say gap of eight maybe that'll be good okay so now we have a link for all files and the reason I did this is so that we can potentially have subnavigation so like this is the main Dash board where we view all of our files and this could actually be changed to a link so over here um I think I could just put a link here and say h is equal to the dashboard and this will be a next link component so let's go ahead and bring it in I'm not sure why it's saying link is not defined Okay cool so the reason I did this again is I wanted to add a favorite so let's add another link here and we will say favorites and then we'll go over here and we'll call this favorites and this will be a star icon there you go I probably want to add a little bit of spacing between these buttons they're a little tightly squeezed so we can say Flex call and we can say gap of four kind of space those out looks a little bit better and then when you click favorites it should take you to a page that doesn't yet exist okay so we need to kind of restructure our code a little bit to support um I think nested routes so the way that works in nextjs is inside of here you can just make a page we can call one favorites and we can also call one all files and then what we could do um also what I'm going to do is like I might this whole thing in a dashboard okay so we're going to put both of those inside a dashboard page and then we're going to move some files around so bear with me it might get a little bit messy at times but we're going to figure this out so this will go to dashboard and it'll go to files and I'm actually going to rename this the files and then this one will go to dashboard favorites okay so on the dashboard page we can add a layout which I'm actually just going to copy this one and I'll paste it in here now the way layouts work is that you basically the pages that are here those will get passed into the children of the layout so if we were to just pretend that um our main page let grab a lot of this code let's just grab it all I'm a fan of just copying everything and then like deleting stuff we don't need so paste this all in and then over here this is where we're going to put our children and then children are get passed in because it's a layout I'll call this a dash board layout that uh metadata we don't really need for right now just geid to some of this stuff okay and then for these links definitely need to import those need to import the buttons need to import the file icon and the star icon and so we can use this layout when we go to like dashboard SL files or dashboard SL favorit so for the files page we're going to basically copy this existing page and I'll put it here and we're going to rename some stuff I'll call this the files page and the main difference here is that like we don't need the side anymore so I can actually delete basically all this stuff remember the layout is going to be responsible for all that so let's just go ahead and delete those two that should be good since we did Nest this too deep we have to um kind of prefix these to make them in the right location so I can just go ahead and do that okay and then for favorites we'll just make a blank page for now I'll say page. TSX I'll say export default export default function favorites page and that can just return a div all right and then for this main page that we're on um some stuff is going to change like we don't need to have all this code on of our main in our main page in fact I don't think we even need a a main page here right now so I can actually just delete that whole thing and um some of these components that we made are specifically for the files page so let's just go ahead and move those in such as upload button file card was one of them all right so from moving all these files around we do have a couple of places that we have to fix like the import locations again just add two do dot slashes to all these things that should make it happy um these are actually in the directory I'm not sure why I added those let's go back and we'll do okay for file card add that in all right so let's check if this works I'm going to go to dashboard uh dashboard SL files and that should hopefully show our page loading your images there you go if you click favorites should take us to dashboard favorites and we don't actually have anything that displays here yet so we probably need to display something let's go to our favorites page and then probably display a title very similar to how we did it over here so let's go and find the H1 probably grab this there now you can navigate between them so in regards to like the state of if you've actually clicked on one of these side navigations um let's try to work on that so the way that kind of works is the layout we could actually abstract this to a client component um I could probably just call it like links or something or I can call it side naav so let's make another component here called sv. TSX and we'll say use client and we'll say export function side nav uh let's go ahead and paste that in and return it this go ahead and import some stuff and then let's use that side naav here so I'll just go and say side naav and we're going to delete the existing thing all right and so the reason we're doing this in fact I think I should put the side next to this layout file not up a level so let's fix that real quick um yeah so we're doing this because we want to know what path we're currently on and the way you can get that nextjs you can say const path is equal to use path name like this and then based on the path name so if it's dashboard files or dashboard favorites we could style the link a little bit differently or the button I guess um let's try doing that I'm going to go ahead and just bring something called clsx in where we can give it Flex Gap two and then we're also going to give it an object here where we can basically change the styling of this if it is currently selected so I'm going to go ahead and say like I don't know give it a text of uh text to Blue how about that if we've selected it and then the way you can do this is you need to pass it a Boolean true or false so we can say if path name do includes dashboard files then that'll be selected and we'll do the same thing over here and paste that in and now as we navigate between these hopefully uh it's not working yet hold on I think this one needs to be called favorites okay so now as you click them you see how the side naav is actually updating pretty cool I might actually do blue 500 instead there we go and so as far as what do we want to display on the favorites page I think it's going to look identical to this so maybe we can create like a shared common component but change how the data is passed in so that different pages can pass in different um queries that are using combacks right so is there a way we can kind of abstract that away into a common shared component uh on the dashboard itself I might just make a browser okay and then we might just grab a majority of this code and put it inside of that in fact I actually might make aore components directory here and I might put all of these inside of that and I might get rid of the browser for right now we're going to figure this out uh I'm going to go ahead and make identical AG for files so files AG your files and the way I want this to work I'm going to rename this one to file browser actually and then we can go up here and instead of exporting default of files page I'll say file browser and we can use this on both pages so now if I go over here I could simply add in file browser and import it and then also over here where it has files can import this one so now both pages will look identical um there's an issue with the title up here we probably need to have that passed in so like title will be your files like that and then over here for favorites we'll just do favorites so let's dive into file browser and let's allow that to be passed in a title title of string and then we want to go to the H1 that we have somewhere and we're going to use that title so now we have favorites and your files and the main difference is that one of them needs to have a different type of query than the other so first of all we don't have a way Tok Mark anything as favorite so we should probably add that to the dropdown so if we go to like the file card remember we have these um dropdowns when you click them like this we have a delete option but maybe if we also add a a favorite so let's find the drop down menu for delete let's add one called favorite St icon we'll add that in and we'll have to do something when someone clicks this button we'll figure that out in just a second let's make sure this shows up okay it does um we probably don't want it red though so let's go ahead and get rid of the red and then also I want to add a divider here I think it's like drop down divider menu divider all right let's go to drop down menu and I do believe there's like a separator in one of these drop down separator okay that's what I wanted I wanted just paste that in so that the delete one is a little bit separate from the rest because all right so the idea is if you click on the favorite we probably want this to invoke a convex action so we set a bullan or something to know if this is a favorite or not all right so let's go to the convex files and we're going to say export const toggle favorite and that's going to be a mutation that takes in an args and in fact it's going to look very similar to this so you might as well just like copy everything here and um paste it in and I might actually abstract that to a helper function because it's kind of duplicative um but what we want to do is get the file which we're getting here make sure we have access to it and then we're going to go ahead and mark it um as a favorite for my organization or my user that I'm logged in as so how do we do that well we probably need to make a new table so let's go to our schema real quick and I'm going to go ahead and Mark one called favorites Define table and then we want to link the file ID um and this will actually be called files instead so that'll kind of point this favorites to to this files then we probably want the org ID so that we have a way to separate between organizations and then also the user ID so I'll just say like user ID is equal to v. string actually no I'll say uh ID of users and that'll point this to this users table all right so basically this table is to map a user to a file and we're also going to kind of isolate it by an org ID because you could potentially have multiple organizations in your account you can have multiple favorites in that or so secondly in order to like query this table to see if we already have a favorite record for a file we probably need to add an index and we can actually have multiple Properties or Fields set up in the index right so we can do it by user ID I think we do by user ID and then file ID and then potentially or ID that might allow us to quickly look it up um so we're going to go ahead and just say like user ID file ID or ID and I might actually switch these okay so why are we doing that well it allows us to basically say give me the favorite from the database I can say wait DB sorry context. dbquery we want to get the favorites where the index of this is equal to and we'll pass this a call back then we're to say q. equals user ID and then we have the user ID it's context actually I think this is the first time we done this we don't have the user ID so we need to also look up the user so I'm going to say constant user is equal to await context. dbquery users with index and I promise I'm going to refactor this code in just a second I just want to get like this basic functionality set up um so with index Q there's no indexes on this so we need to add oh we have the token identifier okay so why is this not working let me comment this out I think that's messing up my auto complete with index by token identifier and then we're going to go ahead and say q q do equals token identifier and then I think we have the token identifier should be identity dot token identifier and then we'll get the first one all right so that's getting the actual user record from the database right and the reason we're doing this is because we're attaching favorites to the user ID so if there is no user we should probably throw an error so no user found and then over here we can finally check to see if the favorite exists so we'll do this user ID is going to be user. ID and then we're also going to check the org ID which is going to be file.org ID and then finally we're going to check the file ID and then again we'll do it first just give us that back and so what we're doing here is if there is no favorite we can create one otherwise we can delete one so for creating one we can say await context db. insert and then we're going to insert into favorites and then we're going to insert the file ID the user ID which is user. ID and then also the org ID which is file.org ID okay otherwise we need to delete it so context DB delete and then we'll say delete the uh favorite ID all right that was a lot of code okay it's a lot of code but again we can make utility functions to make this better so what I'm going to do is I'm going to stract away all this and um yeah honestly all this we can abstract away I'm going to make it into a function down here I'm going to say function yeah I'll say function has access to file and what this is going to do is it's going to basically run all this we can make this an async function we can pass in a context which could be a query context or a mutation context and then we also need a file ID which will be um B Dot you can do ID and then we can say files like this okay let's just go ahead and pass in file ID and everything else basically what we're going to do is if we don't have access to some of this stuff we're just going to return null okay so if you're not logged in return null if there is no file by that ID return null if you don't have access return null if there's no user by that identity we can return null as well and then finally we can just return user and the file and that should be good okay so now we can I think reuse this function in place of all of this so we can say const access is equal to as access to file we'll pass it context we'll also pass it ar. file ID and then we can delete all this stuff because we know if that the access is not defined we can simply just throw a new error so we say throw a new convex error no access to file and then if it is defined we have access. user and we also have access. file like this do the same thing over here access. file access. user okay so now this function is easier to read it's less code and then we have this reusable access checker for files that we can actually come over here and reuse for this so verify that the user logged in and they have access to a file so all this code that we had before goes away and we know that if we got null back here we would't have access to delete a file now can we do the same thing over here get files probably not um create file probably not that's good enough right now so now to test this out basically if we go to our file card we need to invoke that toggle function so let's find out up here we can say const favorite equals use mutation api. files dot toggle favorite and I might actually just name this variable the same as that so now what we can do is down here if you click it we can invoke it and we need to pass it the file ID which I believe is already passed in so let's go ahead and click on this and let's click favorite and what that should have done is it should have created a favorite entry if I go to my data over here go to favorites see that we have an entry that worked fine now let's also go here and I'll click it again and that should delete that entry awesome so that's all working perfectly fine and now it's a matter of just like changing this star to be filled in if it's already favored and also having it show up over here in the favorites list so let's go ahead and just favorite one of them let's go to our favorites list and we got to figure out a way to query for all the files that we currently have favorite in okay so let's go to favorites here I'm going to close everything and for the file browser itself what we could potentially do is I'm going to convert this to Ed client and we're going to go ahead and run a query when this page first loads so I'm going say const I'll say files is equal to use Query and then we want to basically write a new query function which we haven't created yet so I'm going to go to the convex files so there's two approaches we could take we can either copy and paste this entire function called like get favorites or we could just add an argument here um to know if we're only looking for favorites or not I might just add an argument that's like favorites v. optional and it's a v of Boolean so if that's passing as true we know that we should only grab the files that we have marked as a favorite and so we could probably just add another query down here to say like if ar. favorites I think what I'll do is I say let uh this will be a let files and then over here we're going to say files is equal to that and at some point we're going to return files and then if we're doing favorites only what we're going to have to do is we're going to have to basically figure out all the favorites that a user has so I'll say con favorites is equal to await context. dbquery we want to get all the favorites so the cool thing with an index is you don't have to pass in that third thing in the list you can just pass in the first two and so if we actually just do a query here and say equals user ID is going to be equal to um do we have the user I don't think we have the user so we're going to have to like look that up again so I might come back and refactor this a little bit uh but we could just do that query here to get the user and then the user ID is going to be like this we'll do another equals and we want to get the org ID equal to org. org ID and then we're going to collect okay so that'll give us back all of the favorites are attached to this user and that or ID um also there's an issue if user is not defined which I mean it probably should be but just in case I'll say if there is no user I'll just return files um so now we got all the favorites right we got an array of all the favorites and what we need to do is filter the files more so files is equal to files. filter and we want to only get the files where the favorites list has um that file ID so here is favorites it's a list and I'm going to say if Su include oh wait hold on so one is a favorite favorite dot file ID okay a little bit confusing does that make sense so basically filter down these files and only give me the ones that are favorites now again if there's a lot a lot a lot of files in your system then you probably have to change how you're doing some of these filtering um but this will work for a smaller data set and you can come back and do more load testing later let's see if this works so basically if we do files and I'm going to say h files and then we're going to pass in a couple of things uh we need the org ID we need favorites of cre and then query empty string for right now and the org ID I don't remember how we got that before but I think we did it on the file browser somewhere that comes from someplace actually I don't think I need to do the query here what I need to do is basically just pass in a Boolean and say favorites uh and that'll just be a property I can go over here and say favorites an optional we'll say favorites optional Boolean like that and basically if this thing is defined we'll pass it in I think that should be good enough hopefully test out we probably going to break everything so now all files and favors hey it actually worked cool so now it actually only shows us the ones that are favored if I delete it it's gone go over here I'll favorite that one favorite that one and they show up uh let's commit what we have we've done quite a lot so I'll say like um refactoring into layouts and adding favorites feature now another thing I want to check is like does this still work if I do like CSV and if I were to switch yeah that works awesome so we reuse basically the entire component on both pages but we've written the code in such a way that we can just like pass in different options that have it query for different types of data sets and filter down I think this is pretty cool works pretty well um another thing I would like to do is I want to change this to be a filled-in star if it actually is favored because right now it's hard to know so in order to know if this card this individual file ID has been favored by this user there's a couple ways we can do it we could basically just ask for the entire list of all my favorites and then we can do that same check and in the front end to know when this should be marked as filled in or not um maybe we'll do that approach that might be the easier approach so let's go ahead and do something like this we're going to grab that toggle favorite method and we're going to paste it and the difference is that this is going to be a query and we want to get all the favorites so I'll just go ahead and like change this to or ID and this will be a string and I'll say say get all favorites and I think we have a method get has access to or already and we could probably just pass in ars.org idid there's something else we need to pass to it the token identifier so yeah we should probably pass in the token identifier to this which I think um yeah what do we do how how do we do that we need to do this let's do that token identifier save that okay so get all favorites if we have access to the org I mean if we don't then we should probably just return like an empty array um but if we do let's just go ahead and look up all the favorites by user ID and or ID and right now we don't have the user yet either so we might have to um I think what I'm going to do is write another helper utility because this one is just we're like copy and pasting code everywhere I'm not a fan so has access to order already gets the user what I might actually do is do a similar approach where if there is no user um where else are we calling get user do we call it other places add or to user we call it here and that's us only thing on the web hook where's has access to org being called it's only being called in this file so I think what I'm going to do is instead of using get user because I think it's it's throwing an error and I don't want it to what I'm going to do instead is I'm just going to call this method like this and then I'm going to go ahead and say if there is no user return null uh this will have a context already so I can say where is the identity I can grab this if there's no identity again we can return null and then that get the user and then we basically check that stuff and if there is no access so has access is not defined return null and then finally return user and uh I think that's all we want to do okay so the reason we did that is because like there's a lot of duplicate code that was like happening throughout the code base so instead we're going to say if there's no access here um do that that should be good and fix his other places don't need the identifier anymore don't need that because that method already does it up here we don't need has access to org again the only reason we're doing this is to get the user but we should have user already access. user okay and refactor that one too that should have a user on it should have a file on it oh this will be ars.org idid so we're actually coming back to the function we originally were writing we're just kind of refactoring this a little bit and what this needs to do is just return all of them so I'll say collect and then we could probably just return favorites like that and then this one we don't need to check the identity this one has access to org that should already come back with the user so I don't need to do this check anymore I can just say Okay so that's kind of reusing another utility function that should hopefully help us out um a lot of changes I don't know if the app's going to work anymore but let's try it out I can go here look CL click on that okay add it back yeah seems like it's still working that's pretty cool all right so again the whole reason we did this is because we wanted to add a new method called get all favorites that we can invoke from the front end and basically pass that in so if I were to go back to the browser in the file browser we want to get all the favorites so let's just go and say use Query and we're going to say API files get all favorites and this thing needs to take in an org ID which we have down here so let's actually move this like that and there's an overload on this so I'm going to go ahead and say like I might rename this a filter for favorites or favorites only maybe that'll be a better name because let's go to the favorites page and that one needs to be favorites only there we go and I think what we need to do is we're going to skip if org ID is not defined uh we're going to go ahead and just like skip this entire thing I need to do this we don't need any of that and this is a Boolean now so I think I need to change that a little bit okay so now the query should come back and we should get an array of all the favorites that this user in this org are associated with right and so we could probably pass that down into the file cards I'll just go ahead and just pass it directly in so that it has access to all the favorites so this will be um we'll say favorites and this will be a doc of favorites and it'll be an array so now the reason we're doing all that is so we can actually make a function say like function is favored and it's going to take in a file maybe it just needs the file ID I'll just do like a ID files and then basically just return if any of these do file ID is equal to the file ID that was passed in I go and make this a file ID um that'll return true or false right if one of these is favored um it should yeah that should work right anyway let's go and try to use this so somewhere down here we have the actions okay now for the actions we're going to pass in a buling called is favored like this um and this will just be a function that the file actions can call actually this going to be computed is favored could be a computed instead of a function I don't think there's a reason to call it as a function I think we can just do this and this will be file dot file ID and then we're going to pass that in and look here and is favored we'll pass it as a Boolean and honestly like the reason we're doing this again is like we want to toggle the icon here so if it is favored we'll do a star po I don't know if there is a Stars full they have a star half I mean let's just roll with that cuz I'm not going to bring another icon Library just for this so let's go ahead and just bring this in like this and we'll give it the same class and then basically we will just toggle that if it's favored we'll do a star icon if it's not we'll do a star half and then let's fix this typescript error what is this complaining about out oh one is okay file ID is not what we want we want we want ID here thank you typescript all right what else are we uh messing up here so this could be potentially undefined and technically if it's undefined we'll just pass an empty array I think that'll be a cleaner approach to that all right let's try this out if this thing works I will be amazed so let's just go ahead and click this and see okay this actually is fully starred and this is half starred so now if I click it it's fully starred now I'm going to change the text because I don't think the text is uh very apparent what happens when you click it so I'm actually going to go back to the file card and I'll find that halfar let's wrap this in a fragment okay so if it's a star icon if it is favored I'll say unfavorite otherwise we'll say favorite and now this thing has to probably be like wrapped in a div let's see how this looks uh this needs to be fixed so this needs to be Flex Gap one items Center like that that looks pretty good I'm going to go ahead and commit I'm going to say display favorite icon based on status so another thing I think would be really cool to add in would be the way to um basically hide this delete button unless you are like an admin or a moderator on this organization um CU right now I think I am an admin if I go to Clerk and go to my user I think you'll see that I am an admin of the organization which is here okay but if I were to add a user and then they shouldn't be able to delete it unless they promoted to an admin as well so let's try doing that I'm actually going to log out and see if I can just assign them to the same organization so let's go to organizations I'm going to manually do this I think U my awesome team is what we could potentially use together and then I'm going to go to members I think if I go and click add member and um just add my own personal account here and we're just going to give them a member access not an admin access and go ahead and add that in okay so now I have an admin and I have a member you can also add more roles I think I talked about that at the beginning and so if you have more fine grain rolles you might need in your system you can do that but if I were to go to my awesome team up here and go ahead and upload a file some file go ahead and select this image I guess click submit you'll see that I have access to delete it maybe we don't maybe we don't want access okay and so clerk has a built-in component called protect that which you can use I think to kind of like show something else where like you don't have access to delete or something let's try this out to um just basically hide certain things so with nextjs you basically import this protect component and you wrap whatever you want to hide with it so remember we have a delete and we're going to hide it based on if the user has access to it so let's just go ahead and bring that in and we need to make sure that you are a certain permission which I don't know what it is I have to go look this up I think if I go go to organizations and then go to actually let's go to organization settings go to roles and we want org admin I think we can just do roll here actually and then I'm going to say roll admin is that what it is org admin org admin let's try that out um and then for fallback I'm actually just going to return a fragment we just want to hide this completely like don't show the delete button unless you have permission to be an admin and so now you'll notice it's actually gone which is pretty cool um there is that little horizontal ruling though that we got to get rid of so let's just go ahead and I'm going to slap that in there too that should be good so now if I were to log in or give that person Administration access to that org it'll show up but there's also another thing we have to do and that is the backend we need to make sure that the backend still checks roles because if someone is smart enough they go to your network Tab and they could just make a request to delete that item um and we don't have that set in yet so I think we're going to have to go back to the web Hooks and we're going to have to figure out a way to basically figure out what role the user is um granted when they are added to the organization and I think we go back to the event catalog organization created might have the rooll in here maybe sorry organization membership where's membership organization membership created over here is there a role anywhere there is there's a role of admin so that's nested inside a data roll yeah so what we could do is when we get a web hook um so remember the web hooks are in that HTTP over here when a membership is created um we could just go ahead and store the RO and that could be result. dat. roll um and then we need to basically keep track of that in comvex right so users I'll say roll I'll say v. string um actually for right now let's keep this as a v. Union and then I'll say v. literal and that's going to be a admin or it's going to be a v. literal of a member I think those are the two things that we have and then I'm going to pull this out to rolls and I'll say export con rolls is equal to that so that we can use that type elsewhere in our code base if we want to um yeah so now we got to update this to take in a ro which will be at rolls from the schema we just created this thing when I create a user you know now that I think about it we don't want to put the role on the user we need to put the role on the or ID so I think instead of just saying an array this is an array of strings I think we might need to make this an array of object that has an org ID which is a string and then also has a roll which is a Rolls here but then we can get fine grain knowledge of like are you a member or an admin of that role and there's also the edge case of like personal accounts I'll have to figure that out um but yeah let's go back to here and so the way this works is we basically need to append a new object here so I'll say org ID and then I'll say roll is roll okay so that's going to work for patching the user but also here um actually I think that should be fine but then over here for has access user. org IDs we actually need to say Su and I'll say item item. org ID is equal to or ID all right so hopefully that's good and then over here roll what's what's compling about this I'm not sure what the difference between that is um but what I might do is just say if it's admin then set it to admin otherwise set it to member to get this working I think we may have to go and just like delete some of this or ID access um because right now it's like a string but really it should be an array of things okay and then manually we'll go in the clerk and we're just going to go ahead and just like delete people from organizations for right now um let's go to organizations this has two members so let's actually delete this one and remove them and then I'm going to add them back and I think that should fire off the websocket event and then that should hopefully update them to have that or ID which it does awesome there you have it they are a member if I do the same thing with this I'm going to delete this uh there must be at least one organization member with minimum required permissions uh I'll make him an admin for right now because I do want to just delete this one and then I'm going to add them back like this they're going to be an admin and then they're going to be a member again these are just workarounds obviously you you'd have to listen into more websocket events I think there's one for when your membership is updated and not necessarily created um but I think you get the gist and so now if we go to our app and you notice that the delete button doesn't show up for this user which is good but we haven't updated the backend to verify that so for example when someone tries to delete an item or delete a file what we need to do is probably get back the organizations that they have access to and also get their roles so has access to file right now that's calling has access to org which is calling this and it Returns the user so we do have the user already so we can just go back to delete I believe and access should have user and then it should have or IDs and we what we could do I'll say is admin equal to sum or org.org ID uh actually no we want to look up so I'm going to say find find where the org ID is equal to access. file.org ID and then once we found it we're going to make sure the role is equal to admin okay and if we're not then probably say you don't have access so I'll just throw an error saying you no access to file or you have no admin access to delete will this work I'm not sure let's try it out so again we have to go back to the card and find that protect method and I'm actually just going to comment it out for right now so we can test this what we should expect to see is the backend throw some type of error because we're trying to delete a file that we don't have access to so delete it we get an error back it says you have no admin access and now if I were to go back and give that user admin access I I think it would' be nice to actually add in the web hook for getting that event it's just going to be more work but let's not be lazy let's let's add it in so let's add in another event for organization membership updated so let's go ahead and add that in save that and then we got to go back to this switch statement and we're going to go ahead and just check for updated and um the difference is we'll have to make another method called update rooll in org or user and basically what we need to do is pass in the Org the rooll and uh yeah this should probably run the same stuff so let's go to this method probably won't be too hard to add in I don't think just go ahead and copy and paste that and the difference is is we're going to have to basically find the org ID so const uh org is equal to user. org IDs find find it where it's equal to org.org ID okay and we can just mutate it so I'll say roll is equal to args do roll and then we can just go ahead and Patch it back so I think we could simply just say user dot or guides um this one if there is no role here I guess we could say if no org then we should probably throw new convex error expected an org on the user but was not found when updating and that should ow us to like not put that conditional there all right moment of truth let's save these things and then let's go ahead and update the user let's go to organizations let's go here let's go to members I'm going to change this account to an admin go to data and then I'm going to look at I don't know which one I need to look at um but they both say member so that's probably not good right they should both be saying admin go to logs update roll and org for user let's just console log this out conso log result. dat. roll maybe it's just never changing for some reason um so let's go back to clerk we change this one to member and I'll go over here and it printed out org of member all right so I think I just need to change this a little bit I need to say if roll is org of admin then we will be admin otherwise we'll be a member let's save this just double check if I switch back to admin get or admin go to data go to our users and you can see that now one is admin the other is member so I do just need to like force that one back to be member and that should be good okay so all of that because what I'm trying to do is I want to show you that if I try to delete it it'll fail okay so that did fail you have no admin access to delete but if I were to go back to Clerk and give this user admin access go over here and uh wait a little bit for the web stocket event to come in and then click delete I can delete it now awesome but I think we made some good progress on that I'm going to go ahead and just add in um ability to change user roles and protect the delete functionality so in many of these file upload and storage systems they actually don't allow you to automatically just delete an item what they normally do is they mark it for deletion and then some type of Cron job will come through and delete it after 30 days or after a couple of weeks or something like that so I might actually try to add a trash subnavigation where you can still view the files that are like marked for deletion but convex has a really cool way to basically set up Chron jobs that run periodically and we can just Loop through all the items that need to be deleted and just go ahead and delete them permanently from our system if we need to all right so first of all let's go to the layout of the dashboard and let's let's go to the side nav and over here I'm going to copy another link and I'll call it deleted or I'll call it trash actually um and this could go to a trash dashboard and we want to check to see if the link has sltr in it then on the dashboard what we need to do is make another directory called trash and then inside of the trash we can do something similar to favorites where we're going to have like um maybe some type of buling that only grabs the items that have been marked for deletion um yeah so let's go ahead and try doing that so in the file browser I'm going to say delete only deleted only maybe that'll be a better name for it and then I'm going to go ahead and just go in the file browser and I'm going to go ahead and just pass that in as again an optional Boolean and the difference is we're going to go ahead and just pass that in and that thing is not defined yet so what we need to do is I'm going to go to that git files method and that's going to be a v optional V Boolean and then scrolling down we're going to modify this and also just say if we only want deleted only then what we could do is just filter out the ones that are is deleted or is marked for I'll call it should delete sometimes I jump around with what the name stuff um okay so now is um should delete should probably be attached to the file itself so let's go to the schema and this will be a Boolean which I'm going to make it optional V optional for right now and basically if this thing is set to True when the crown job runs is just going to delete the item okay now technically we should probably have like a delete at Tim stamp 30 days to a time um maybe we can do that I think I going to keep it simple right now so I can show you the cool ability that conix has for cron jobs all right so now let's go back to the trash and then that should only show deleted files now the difference is if we go to files when you mark a file to be deleted what we need to do is instead of deleting it from the database we're actually just going to update it we're going to say context DB and then we'll say p args do file ID and then we're going to go ahead and just say should delete is true like that so now that what that will do is um should put it in the deleted or the trash uh section secondly with the snav we need to change that to a trash icon trash icon here we go and if you click it it it should take us a trash right now we don't have anything marked for deletion so let's just go ahead and Mark this image for deletion and um you'll see down at the bottom at the bottom this modal actually needs to be renamed a little bit your file is gone from our system uh instead of saying gone from our system I'm going to say file marked for deletion your file will be deleted soon okay and then secondly this action will Mark the file for our deletion process files are deleted periodically I'll just kind of make that description a little bit better and then if I go to trash it does show up here so the issue is that um it probably shouldn't show up in all files after we've marked it as deleted so I'm going to go back to the files file that we have and um where we do get files basically um we want to filter out the deleted files down here so if it's deleted only then we only want the deleted otherwise we're going to say do the opposite of that I'm just going to go ahead and say give me the ones that are not should delete and then return that so now if I go back to all files there it's gone now so now we only have the ones in trash now we probably want to an option for restoring it so if the item has already been deleted we probably want to allow us to like restore it so that it won't get deleted in the future so let's go ahead and add another method here instead of delete file we're going to add a restore file restore file and that's going to do all the same logic basically but we're going to say should delete is false okay and then we can call that that from the card let's go to the card and let's find that delete method and we're basically going to say if file dot should delete is true then we're going to add a restore icon which I don't know if there is like a restore icon um maybe undo icon might be pretty cool and then we'll put some text that says restore and then otherwise I do need to do need to put this class on it so it looks better and then I probably need to wrap this whole thing in a div like this put that div over there and then otherwise we want to let's just do this all right save that okay there now instead of uh red I'm going to do green for ReStore and I'll get rid of the text here okay so now it should say restore and when you click it instead of calling delete um well instead of opening this modal again I'm just going to say if file dot should delete otherwise we'll open up the confirmation and then if it already is deleted or in the the marked as deleted all we need to do is just go ahead and bring in restore file like this and then I'm going to call it we'll say file ID is equal to file. ID and then that should bring it back from that state okay so let's just go and click restore and now it's added back over here awesome okay so now in terms of uh we definitely need to add back the protect that I deleted on accident so let's add that back and that should probably wrap both the delete and the restore when it comes to convex they have a really cool Cron job ability where I can just make a Cron's file like this and we can basically have it run some type of action or mutation every hour or every minute let's just make a cr. TS and then I'm going to go ahead and just add in a interval uh actually I'm going to grab all this okay and so for right now we'll do every minute but what we want to have it do is call some type of method for deleting the file for real this time so let's find delete file and we're going to make another one above this we'll say export const um delete all files equals an internal mutation cuz because we only want to have this called from that KRON job go ahead and copy some of this stuff and we don't need to check access because we know it's only ever going to be called from a convex action or a a Chron job right so let's get all the files I'm going to say files get them all and then I'll say await context DB query give me all the files and in this point we probably want to add some type of uh index to the files to know which ones need to deleted so I'm going to go ahead and just add an index here I'll say bu should delete and then we're going to go ahead and say should delete okay so that allows us to basically query based on that index so should delete q q. equals should delete and we want to make sure that it's true and then we'll say collect and now what we can do is we just Loop through every file so for I'll say a wait promise.all um we'll do files. map file and then we basically want to just return a wait context DP delete file uncore of ID okay so that will basically do a batch right and just delete all those records but the other thing we need to do is we're going to have to return I'm sorry we're going to have to delete the um the actual files from the storage right so I say context storage. delete file. file ID so delete from Storage first and then delete the actual entry and um yeah that should be good I hope let's go ahead and do that now this will work for a majority of cases right if you have too many files like let's say you're trying to delete thousands at a time you're going to have to break this up into smaller chunks and process them individually um like batches of 5,000 at a time or something but overall I think internal mutations have a time out of like 10 or 15 minutes or something so what we're going to do is from that KRON we're going to call that from files and then we'll just call that every minute now let's look at what this is complaining about args were not provided um I don't need to pass any file IDs and then down here we have to export the crons make sure you don't forget that so export it and now the cron should be enabled so I'm going to say delete any old files marked for deletion and let's just go ahead and Mark a couple for deletion so let's go here and I'll add this one oh notice that the delete icon got messed up so let's go back to that card and I think this needs to be a trash icon I'll mark that for deletion I'll just delete them all why not then we'll go to our trash and we'll just sit we'll just wait here and at some point it should delete both of these from our system there you have it they're both gone so the Cron job fired off it fires off every minute you could change that to be every day if you want to um and then you can just let the user know that like hey we delete these every day in the morning at 9:00 a.m. or you could do that approach where I have like a deleted at Tim stamp or a deletion timestamp and then basically the Kon just fires every hour checks things that need to be deleted and clears them out so I thought this was pretty cool like this is something that's just built in the comvec so you don't have to worry about it all right so the next thing I want to add is if different people in your organization upload files it' be nice to see like who uploaded it and also like what date time they uploaded it at so that's what we're going to try to work on now unfortunately we didn't pull over the username um when someone registers and clerk right so we got to go back to that web hook and basically when a user is created we probably want to pull over some additional information and pass it over for create user okay so we probably want like the name of the person so I could say result. dat dot um I think there's a first name we could probably use and then technically we could do like a result data do last name and if it's not defined we could just go ahead and just the these to something else right now okay so we got the name also the profile image we probably want to pull over so I'll say image that's result. dat. image URL so we're trying to send over name and image to the mutation but the mutation doesn't know about it so let's go here to this internal mutation it's going to take a name it's going to take an image and that could be also a string all right and so we definitely want to store those things so we'll say name is ar. name name and image is ar. image like that which means we have to go to our schema and we should probably add those in so I'm going to say name and I think these typically might need to be optional um but we'll just do that right now so v. optional v. string and so we're going to store those over when the user were to create their account now we could also listen for another event called user updated I believe cuz you can update your image in Clerk and we probably want to like listen for that update so let's just go ahead and copy that and I think it's called user updated and um what we could do is make another internal mutation called update user like that let's go back to users and we are going to just copy this whole thing we're going to say update user I'm taking the same arguments as before but now we got to get the user first so I'm going to say await context DB uh query users and then we want to say with index and then we want to say token identifier we'll say q q. equals token identifier args do token identifier that'll give us back to user I can just go ahead and say first if there is no user we probably want to throw a new comvex error uh no user with this token found and then finally we have the user we can just update them so I'll say DB patch pass in the user's ID and then we're going to pass in the name and the image and so now that we added this user updated um web hook we got to come back in the clerk and we got to make sure we add that to the subscribed events so let's go down to user where's that user updated just click that click save then we should be good on that front um the one thing you'll notice is that this thing is hardcoded like four different places not the best what we should probably do is copy that and we're going to go to comvex and we're going to add that as an environment variable I'll just call it clerk poost name paste that in save it and now we have access to go over and use that so I'll say process EnV clerk host name so that we don't have this thing copied and pasted in multiple places and then I'm going to go ahead and just grab this like that so we we did a lot of changes let's make sure that this actually works so we're going to go back to our app I want to click this I want to manage my account and I want to change my profile image first of all and see if this will get updated so let's just go ahead and select an image I'll just select this starting soon image click continue and then we should get a web hook event if I go over here we get a users update user event that's good go to data go to users and now we have an image and a name name on that user itself and so the reason we did this is so that in our app as users are uploading information we have that latest up to- dat data about the user so we can go to the card and probably grab that user information okay so going to the file card what I want to do is I want to display the user who uploaded this okay now right now it's not on the file information so we're probably going to have to either have the UI fetch all the individual users information or we can just kind of like include that on the original data that comes over from the backend um now what I actually might do is I'm just going to fetch it from the UI so every file card what we could do is do a query to get the user information so I can say like user profile and then I'm going to make a new endpoint called users. get user profile okay so let's go to convex we already have a users file here and I'm going to make a public query export const it user profile is a query might have to import that got some args AC Candler context args and um so what we want to do again is based on the user ID that was passed in we can go ahead and fetch the user information so I'm going say const user is equal to wait context DB get and we could just pass in the user ID like this and that'll give us back a user but in case there's like sensitive information on this such as like the token identifier well that's not sensitive but the or ID maybe we're just going to return name and image okay okay so now we have a method we can query from the front end to actually display that information on a card by card basis so for every card we already have what user is attached to the file so we should be able to say file dot um oh we don't have a user ID on the file so we're going to have to go back and add that so we're going to put a pin on this we'll come back we're going to go to schema we're going to go to files and we need the user ID stored here so I'll say the ID of users go ahead and save that but now um we'll have to go back and update every single place where we forgot to store the user information so here we should store user ID we can say has access hopefully user's on there it is there we go so now we're storing the user ID on these files as they're uploaded and then we should have access to that here I can just say user ID and then we'll get back that user information here what is this complaining about it's saying user ID is in args oh I'm I'm calling use mutation this needs to be used query now you might say okay let's say we have 100 cards on the screen the way convex works is it going to dup your requests so it's not going to make like a th000 requests or 100 requests for every single user it's only going to make a request once cash that information locally and then you'll have that on anywhere else where you have that user ID so let's go over here somewhere in the card where we need to display it in fact I think I pulled that in to the wrong place I think I need to actually pull this out I need to go down here do the actual card and that's where we need to pull that data in because down in the card footer what I'm going to do is I'm going to display the name and then also the image needs to be used somehow now we haven't brought in an avatar component I don't think so let's let's go to Shaden and we're going to bring that in okay so let's go to Avatar and then we're going to just go ahead and install this so we can use it okay so while that's being added go look at how it's used so you basically import these things ahead and just import them here and then we are going to grab over the Avatar which allows you to basically put a link to an image so I'm going to go ahead and just put this here and I do think we need to update our nextjs config to allow whatever URL it's going to pull in because it's probably not configured um and then technically here you could try to like look at the name of the user and like put uh their first name last name uh characters here but we'll leave it like that I'm going to go ahead and just put the name here and let's see if this information displays I think there's some things we might have to change for example um we just made some required fields on the files so let's just delete our all of our files and start fresh let's go ahead and refresh I'm going to go ahead and upload a file here testing we're going to grab this image click submit and there you have it uploaded by me and and we have this image over here and so just to verify this is working what we could do is just like update our image one more time um upload a new image think I should have a web def Cody icon here yep there we go save it we should get the web hook event that should come back and see that our image just updated live on this file cool um so again let's just style this up a little bit I want to add some more space between the name and the icon here so I'm going to remove the download button and I actually want to put it in the dropdown I think it'd be better if it's not there so let's go to where we display um the favorite and unfavorite stuff and we're just going to go ahead and add another drop down menu item go ahead and copy this whole thing and we're going to go ahead and just say download proba use like a file icon here and then when you click it we probably also just want to do this type of logic okay so now the download button is n on this thing we can go here and we can just download it directly from clicking on the drop down I think this is like a better user experience now for this I do want to put some gap between here so let's find the Avatar and let's go ahead and just give it like a class name uh actually no the footer we'll give it like a gap of two there we go thank for the Avatar maybe we can actually reduce the height of this a little bit there we go I'm also going to align this to the left instead of centered so let's get rid of justify Center and uh I think that should push it to the left might make it smaller and then here I'll say text of extra small uh I think that's even too small is there a smaller one than extra small they're like an extra extra small um one thing you can do in tail one is actually to specify pixels so I can just do like text 12 pixels and that should make it pretty small I think and then for the actual text here I'm going to say text of extra small and then text Gray of let's do 700 so it's a little bit smaller it's a little grayed out um yeah so secondly when was this thing created we should probably display that over on the right um so let's go down and we can say like uploaded on and then we have the date it was created so I can say creation time it's probably not going to be formatted too nice so we'll have to go and like bring in a date library to format that yeah so just a couple things here I'm going to go ahead and just wrap this in a div and then I'm going to wrap this one in a div like that and then for here I'm going to go ahead and just pull this class out here I'll say class name and I'll just go ahead and say Flex justify between that should push them out and then the uploaded on again could be a text of extra small there we go and then for this entire thing you could probably give it like a hard-coded width of like 20 just so it has more real estate maybe 40 okay we'll do items Center on here on this div I'm going to go ahead and say items Center and then okay let's let's format this day cuz that doesn't look too good so when date Library I like to use is date FNS but there's different um date libraries out there basically how do you install this install it with date FNS so I'm going to go back to my terminal down here mpm install that date FNS Library actually let's just grab this one the format relative so we should be able to basically take that date here and I'm going to wrap it um I'll do the creation date here and we're going to pass in the date into that go ahead and Auto Import format relative and the other ones too so I'll just go up here paste that in and I don't think we need sub dayss let's go rid sub days okay so what this is doing is basically saying take the current date that this was created compare it against the date now and if we did this correctly says uploaded today at 5:40 which is pretty accurate because that's what time I uploaded it uh let's change this to gray text so I'll go ahead and just say like text Gray of 700 as well okay so again the reason we're doing this is because if I were to log out and log back into someone else we should see their names pop up let's just try that I'm going go ahead and switch over to another team because that's the organization that we care about go ahead and upload another screenshot and we'll grab from the desktop we'll grab some screenshot there submit it and you'll see that my name in the image doesn't show up because I think I I added the web hook after I've already kind of created this account so I'm just going to do a little hack here I'm going to go a and just take a screenshot I'm going to go ahead and just upload an image do that click continue the web hook should send over and then now that is showing my name and when was uploaded all right so again let's just do a little bit more cleanup so I think this text is just a little too big right when you're trying to name your files and they're like going off of your um card probably a sign that it's just too much text let's find where that name is okay it's right here I'm just going to do a text of Base uh actually I think I could put it here text base and font normal I don't want it to be bold anymore let here the Bolding and that looks better in my opinion now if you wanted to you can kind of truncate it and maybe do like a Max characters of 20 and then add dot dot dot if it's too long that's an approach I've seen in different applications but this is coming along I think this is pretty good now let's fix let's fix these I think the icon's a little too big let's figure out where we did the file icon which was here and I think we could probably just give this a class name the width of four height of four and that'll match the other icons okay secondly let's think about the ux the most important thing that you probably need to do is download the file so let's go ahead and Swap this to be above favorite because I think downloading is probably the most important thing you should have it be first I apologize if I'm jumping around but this is typically what I end up doing like I just want to kind of Polish up here and there and it's just cool to just do it as you see you need to kind of update these things um as you may know if you watch my tutorials I don't have like a strong plan I just start building stuff so hopefully you guys aren't lost when I'm doing this but I think it's a A Better Learning Experience um up here I think if we're building a real application it's good to have a logo or some type of symbol of what your thing is so I went ahead and add one to the public folder here it's called logo so this is a the file Drive logo we're going to use let's go ahead and find that navbar um or do we call it header I think we call it header and then in the file Drive div here probably good if we bring in next image we'll do source and we'll do logo.png uh with maybe we'll do 80 pixels height to be 80 pixels and then also an ALT could say file Drive logo ohol now on the div itself again Flex we'll say gap of two and then we'll say justify sorry item Center I think that's should be good there we go now that image is huge so we're going to scale it down maybe 50 instead of 80 and drop that down even that's a little bit big so let's just do 30 no let's do 40 there it's it's kind of too hard to even make out actually so maybe we could do a better better logo um but this could be text of extra large maybe and now we have like an actual logo for our application second thing is when you click this this whole thing should be clickable and it should take you back to the homepage that's just like a convention that every website does so instead of a div here um we could technically have a link like this which an hre of the home so now I can click on this and go to our homepage but remember we did some refactoring a while back and we don't even have a homepage anymore so at some point we'll have to come back and fix that in fact now that I think about it it might be good to have a link here called dashboard or manage or something so in the header itself we could probably say add another link here here hre will be called slash dashboard files and then we'll just say your files okay and then we can click this and it should take us to this link um it might be better though if this was a um a button let's bring in a button and see if that works and then secondly the variant here we can try doing like a ghost variant or an outline might be good outline there you go I think that's a little bit better so it's not as like abrupt in your face but you can still see it's like a link that you can click and then let's just go ahead and commit we haven't committed in a while but let's just go ahead and add everything and say logo and header then I'll say display creation time and user Avatar in cards move download button did a lot of changes I'm going to go ahead and sync those all those up what I've seen in some applications is this card view is pretty cool but sometimes you just want to Table Right sometimes just seeing all your files in a table format is a better user experience for some people depending on what you're trying to search for so Shad Cen has an example of how you can kind of use a data table this is what a data table kind of looks like where you can kind of filter out columns based on different um names you can add actions to them so like for example downloading a file could be useful from this drop down um deleting it you can do multi selects like this so let's try to bring this in or at least bring in a subset of of this there's also sorting as well so let's just try to do it pagination included but I don't think we're going to use pagination because it just makes things more complicated so let's try adding this table all right um but then we also need something called tan stack react table so this is using a third party library to basically get all these nice sorting features set up we're we're going to install that as well okay so how do we set this up so let's just follow the guide PR Rex we are going to build a table to show the receipt payments here's what our data looks like um so we'll need to create some type of data uh and what we're going to do is we're going to find our dashboard page so file browser probably also needs a toggle which can switch over to a file table so let's just make a file table and let's just kind of pull over some of this data here they have a columns so I can just make one called columns I guess and let's just go ahead and copy this whole file and then they have the data table which I think we could just probably put directly in file table so I'm going to go ahead and copy this like this there you go and then it says you have to render out table so somewhere here we have to bring in data table let's go to our file browser and for right now we're going to do it right above this grid stuff so I'm going to go and just import our data table here and we have to pass it columns and we have to pass it data um which I think we have to Define up here so columns we can import directly from that other file but for data we have to actually pass in the files that we're loading so I think we have to pass in files here and um we might have some typescript errors so we got to go and like figure out how to refactor this for columns this is going to be a doc of files like that and then we want to basically show uh I forget what's on the files so we have like the name so we definitely need a name like this also probably have like a user table um maybe a type might be useful you know we won't use user for right now cuz I think we're going have to refactor a little bit of code but I think this should be good payment we don't even need anymore I don't think so let's try this out if we go to our UI is this even working yet there we have a table it shows testing and it shows another screenshot so that's working fine we have the type okay so now we have the table displaying I think what we want to do is display a column for like when it was created kind of like we're doing down here so let's go and add another one called uh header could be uploaded on but I believe we want to actually like have a custom function for how this one is rendered out and I don't remember how to do this let's go back to the data table and I believe you can use a cell and then you can manually return something here so like if we want to return the created at date that's formatted we should be able to do that we just need to be able to get the created on or created at timestamp I forget what it's called we did it on the other page we might as well go find that it was here creation times yeah let's just grab this and then we're going to go back to here and we want to actually just show that I'm going to do format relative and um I think we can say row. original. creation time if you want type safety might be a little bit easier than whatever approach they were trying to get us to do so let's see if this works you may have to give an accessor key I'm not too sure seems like that worked fine it's displaying okay so the the cool thing is you can actually like customize what's inside so it's as simple as that um another thing we want is who uploaded it so like we should probably have like a user let's copy this approach and we'll do that before the uploaded on so I'll say like user and we want to get the row. original dot user ID so we're going to have to do another look up here um so I'm going to say const user profile is equal to use Query I believe we can do this here use Query go ahead and import that api. users. user profile user ID is equal to row original user ID like this now this is a little issue with um you can't just do this right here so what we're actually going to do is we're going to pull this out to a custom function I will say function user column or I guess I could call it user cell and then we're going to go ahead and move all this stuff out like this that we don't get that es lint error and really all this thing needs is a user ID which is a v do um or ID I could do ID of users okay and then we're going to go ahead and just use that and now we got the profile back at some point so we should be able to get the name displayed so we're going to use that custom component and we're just going to display it right here okay so that we cannot get that es lint error anymore and again we're have to pass in user ID so row original do user ID all right let's try it out so now it says user Cody okay and we can also probably display the image here so how do we do that well we'll have to go back and find the Avatar let's go to the file card let's find the Avatar in fact I'm just going to grab this whole thing because it's going to look very similar to that and we can go back to user cell we can go ahead and just paste that whole thing in Auto Import a couple things and it should show up now hopefully there we go all right we also want an actions column so how do we add an actions column well we could just go ahead and add another definition here we can call it actions now when it comes to the actions on the table we actually want to just pull in that exact same drop down menu that was like on the card itself so I think what we could do is probably pull this whole out into another file like I'll just pull all this out and we'll make a component called file actions maybe and then we'll paste that all in and then we're going to go ahead and Export it so we can use it in both places so export that and then let's just delete anything that's unused TR to clean up the code a little bit okay so so now if we go back to file card we got rid of file card actions because it's in another file so we can delete that this we can delete and then we need to Auto Import it over here and technically this git file URL could probably have been um exported as well so let's export that and then over here we can import it and then again let's just go and delete any unused Methods at the top here just to keep our code clean cool um now for the table instead of showing this we can say file actions and this thing needs a couple things so like let's figure out what it needs first of all it needs a file which I think will be row. original and then it needs an is favored which for an now to say false uh we'll have to come figure that out in a bit where that comes from yeah let's let's look at the UI does this still work he that's still working I got the actions over here I can do that I can delete we go back over to the trash and restore make sure that works awesome so we got some shared functionality between the table and the card They will be uniform as we're updating stuff now I think the favorite might be a little bit messed up here we have to figure out where that comes from so in the file card itself we have like is favorited and so I don't know how we're going to get that passed in because I think we have the files here which calls the file browser this is going to have the favorites and then that needs to probably be pass pass into the data table somehow we have columns over here I think actually if instead of passing favorites in here maybe we could just attach is favorited to the file itself so like right now we're passing this in then we have this computed If instead we went over here and we said um modified files and we just go ahead and say files. map and then for every file we say is favored and that could be basically running over that sum map like this and then I think we're going to have to get the favorites which we do have let's see favorites is possibly undefined so this would have to be defaulted to an empty array so it doesn't it's not going to crash and then the modified files we could actually pass those in here like this and then here we can Loop over them and delete that now the issue is that now like these things are expecting a dock of file but now they actually have a file but they also have is favored buan I think and then I think for file card we don't need the pass in favorites anymore if that makes that happy okay there now that's happy so let's just grab the same type definition and we're going to go to the data table um actually we're going to go to the columns and that needs to take in our new definition I believe and the reason we're doing that is because we were hardcoding this but now we should be able to say is favorite Ed pass that in and uh we don't need that anymore file. is favored go and fix typescript error so everything's working good um so it's thinking that this could be undefined if you look over the error which it needs to be defined at all times so I think what we want to do is just over here we can just fall back on the empty array to make that happen so let's let's go back here so does this work now if I were to look at favored it's a full star and if I click it again it works okay so now we should have two favored things let's go over here and uh yeah they'll show up and if I unfavorite them they go away so another thing we should fix we don't we obviously don't want to show cards and then a table at the same time we want to allow the user to pick between what view he wants so I think it'd be nice to add like maybe some tabs up here that you can change the view of how you're looking at the files so let's go to tabs and then they have um an example of how you can do account and password here's some code so let's just go ahead and install this like that install that one and then we're going to go ahead and import all of this we'll just import it directly here go ahead and delete some stuff we're not using anymore and then we're going to bring in all these tabs and the tabs need to live underneath the H1 so let's go to the H1 here and directly under it let's just go ahead and add those tabs in and instead of a count we should call it grid grid uh this will be called grid grid and then for password it'll be called table so update all those and what this allows us to do is in our UI we should be able to toggle between them okay and now all we need to do is just basically put the Grid in the table uh inside of these tab contents here so like okay so what we need to do is inside the tab contents we have to grab for example let's grab this grid and we'll put it in the grid View and then let's grab the table we'll put it in the table View and then I think the placeholder we can leave in place that would be good now if you click it it's a little messed up so I'll have to fix that I'm not sure what's going on there um I think the issue is because this has a hard-coded width I actually want to pull this out and put it on the tabl list instead in fact having a hardcoded maybe I don't need a hardcoded width I don't know why they put that in the example it's kind of weird go and delete that save it here we go and then adding some type of icons to these might be beneficial so inside of here we could say like grid icon uh make sure I import that I guess I didn't import it there we go and then also we should probably style this Flex cap 2 item Center there we go same thing with the table let's just go ahead and grabb this whole thing all this Clash right here let's just put it here on the table and then we want to say table icon like that now you know I think about the table icon not good it doesn't look too good um let's try Rose icon there we go navigate between these you can pick and choose which one you want now it will reset every time to grid that's the default view so if you wanted to kind of persist that you'd have to basically um you'd have to store it at a higher state or store it in local storage or session storage if you want those to be persistent I think adding a little bit of margin underneath it would be good let's go here and I'll say class name margin bottom of four just to push it down did that do anything let me try eight yeah that's not like pushing it down at all so I think I actually want to put it on the tabs list yeah there we go cool um another thing I don't like about this app is like when I do a full page refresh it says loading your images let's find that loader loading your images and it should say loading your files because this is a file storage application not an image storage but secondly the spinner should only be like where the you know the table is right so technically it should only be um a little bit lower let's go to the contents I think we want to keep the tabs but for the tabs Contents I think instead we probably want to show like a loader here I don't know if that's going to work let's let's see what happens and then this for is loading here I'm going to get rid of that completely I don't think we want that anymore let's try this let's see if this looks different refresh the page and now the is right where the table in the grid is which I think is a much better user experience all right let's just go ahead and commit this I'm going go and say adding a view toggle with a table view all right so I think another cool thing that we could add is the ability to filter based on types you might have a lot of images in here we have the ability to filter by name but like what if you have bunch of PDFs images csvs and you just want to like quickly just see all the csvs one way we could do it is by using a select so let's go ahead and install a select component from Shaden okay that's installing and then we're going to grab all this input stuff and let's figure out where we can put this because right now I'm not even sure where we put it we probably want to put it on the same row as the tabs list so I think if we put a div here and um let me import those first sorry I'm like jumping around let's import those I'll put a div here this could have another Nessa div this could have a class name of flex and then justify between item Center maybe and now if I say testing here we should see testing pop up in our app over here so if we wanted to filter by type um let's bring in a select like this let's drop that in there and um yeah some things we might want is like image CSV PDF let's see what this looks like okay looks pretty good in fact this one should say placeholder all I think for default value maybe we should put all here and then maybe we don't need a placeholder and then what we want to do is when this thing changes we're going to change the way we're doing our backend query to just give us things that are a certain type we also do that in the front end too as well but we're going to keep this simple I think we can say on value changed we can go ahead and call something and what we need to do is kind of how we did it similar before like we're going to have to keep track of the type and by default it's going to start as all and then we're going to use that state to change how we're doing our backend query so let's go to the select and then we're going to change this instead of default value we're just going to say value is equal to type let me get rid of this and then on value change it'll be set type that hopefully this works I don't know let's click it okay it is changing that's good but what we need to do is also when we say use Query we need to pass in the type that the user might have selected so let's just go here I'll pass in type and then that's going to error out because we don't have a type passed in so let's go over here and we'll say type is a v optional v. string and so like we did with other filters we're going to say if type is defined so down here I'm going to say if args do type is not equal to all then we're going to go ahead and say files is equal to files. filter and we only want the ones that have the same type as ar. type now if you want to get more specific about this we should probably change this to be a v optional of file type so that we know from the front end that like we're passing something incorrectly or not um and then also this all thing I think we could just like not pass it so we're going to keep that and then in the front in we're going to say only pass type if type is equal to all then just pass undefined otherwise pass type like this I'm getting my Turner mixed up here and then I think type needs to be defined up here um so like we need to say file types think we need to say doc files type uh and then also all oh not the end I think we need to do Union all right is this is this going to work unfortunately so this thing is going to pass back a string so technically this is going to be like a new type and then we're going to have to just like cast it so I'm going to say new type as any so that it's not going to be mad at us there's no way to really like link these types with this over here I mean technically you could like maybe you can cast it here and see if that works yeah I think it might need to be a string all right so that was a lot of code changes just so that when we do this and select image um it seems like it's working let's upload some new files because I don't think we even have a CVS uploaded CSV and then also upload a PDF all right so let's filter by image by CSV PDF there we go and I'll probably add a label to this too like we should probably add a label in front of this so I'll say label and then I'll just go ahead and say like type filter um I kind of rather it be on the left so let's go ahead and like give this a flex two and that should put them side by side and then items Center there you go and then if you want this to be proper accessibility Tech technically you should do like an HTML 4 for this HTML 4 and then we'll say type select this ID could have type select I'm actually going to rename this like this here we go I'm not sure why that doesn't have a maybe I need to put it there there we go awesome so now we can filter by the type over here and then we also change the view so one little bug I noticed is that when you are on your personal account and you click on the file you don't have the delete button it doesn't show up so the first thing we're going to do is we're going to change this from checking a roll and we actually change it to a condition so we're going to go ahead and do this and we're going to return um I'll just do check and I'll say roll is equal to or of admin and I believe that's going to be the exact same logic as I did before but it gives us the ability to also overwrite it and check other things so what we want to do is if the user ID of this file is equal to let's just do like me uh yeah we'll do Mi doore ID we're going to make an endpoint that we can call from the front end to get our information for our currently authenticated user so and we want the actual convex ID we don't want like the token identifier so let's go to users here and I'm going to go and say export const get me equals query and then what we want to do is probably give it blank args async Handler context args and then we want to look up the user based on the identifier so like we're probably going to want to do this token ident fire did come from Context I believe so we should just be able to say context off user identity token identifier like this actually where else do we do this do we have like another G user helper uh I'll just do this right now we do like identity equals somewhere identity equals yeah I'll just do this okay we have to be logged in and then we're going to get the user based on the identity so identity dot token identity and then there's no user probably throw back uh probably return null typically on queries I try to return null and on mutations I throw errors otherwise let's just return the user object so this will allows us from the front end we can just get all the information that we need more specifically we're trying to get this underscore ID um but now that we have a query it's going to give us all the information for the currently authenticated user okay so it should be save to call from the front end and then we could just go ahead and invoke this so I'm going to say const me is equal to use Query I'll say api. users. me and uh at some point that should come back and we can check if me. ID equals the user ID of the file that make sense so now that'll show up in the front end I don't think this will work on the back end just yet we have to modify the back in a little bit um yeah there's a little error you have no admin access to delete so let's go and find the files and let's find delete file and this logic needs to be updated just a little bit um we do have the file I believe so access should have the file and I'm going to change this from is admin to can delete and then we're going to basically say you either have to have um access to the file itself so access. file. user ID is equal to access. ususer doid or you have to be an admin of the organization okay so a lot of stuff going on there let's see if this works now I'm going go ahead and just um do it again and there you go it got moved back also restoring the file we'll have to fix too um so let's go back and figure out where else do we check if admin it looks like we do the same logic over here so I think at this point we should probably abstract away this do some helper function so const can delete I'll do function can delete file we're going to return this and this needs to take in a user and then a file these will be Doc users file will be doc files and then we just want to get rid of access a little bit like this get rid of this and then you have no access you have no access to delete this file and delete file so we're going to use that here and we're going to pass in access. user and access. file and that'll figure out if we can delete it or not and um since it does throw an error we don't need to check anything I'm I'm going to go ah a and say like assert and delete file okay and then somewhere else we were kind of doing the same thing uh right here assert and delete file we'll do access. user access. file okay this is why it's good to have smaller utility functions that you can kind of reuse because as you can tell you update it one place and then you forget to update it somewhere else so let's go back and upload a file let's make sure we can delete our own file make sure we can restore our own file there you go and then I'm going to switch over back to um an account that I do have admin access to and then I'm going to delete go back restore so let's go ahead and commit some of the stuff we did a lot of changes there I'm going to go ahead and say fix issue with deleting files on personal account okay definitely important to get that fix in okay so I think the app is pretty good I mean I think we've built quite a lot you have a good understanding of shad C in all the different components how to kind of abstract way components and stuff like that um how to download and do permissions and stuff I think what I want to do is just add a footer at the bottom of this app to make it look a little bit more polished let's go to layout here and let's add a footer and we could probably just do a new footer here footer. TSX export function footer return to div and this div probably could have a height of like 40 something and then maybe also BG a gray of 100 let's see if this shows up I need to make sure that I Auto Import this all right there's the footer now we should probably just do like some margin top on this thing margin top of 12 just so it's spaced down and then typically on a footer you'll have like a privacy policy or some other things so we could just go ahead and try to add that stuff in I'm not actually going to make those pages but I'm just going to show you how you can make like links so we'll just go ahead and put like again uh a div here that says file drive and then we will just put some links here so link href is privacy and I'll say privacy policy go ahead and Auto Import this do like an about um we'll need a terms of service terms of service and then finally maybe just like an about page or something probably good enough let's look at see how this looks this looks pretty bad so let's just go ahead and wrap this whole thing in a container so this thing should probably be in a container MX Auto it so it's centered there we go and then technically we could put all these on the same line so I can just go and say like Flex and then I can say justify between item Center That should kind of space them all out we wanted to Center that whole thing I think we could say Flex item Center here there we go and uh yeah that should be a little bit better now sometimes they put these all together it might be nice to style these buttons a little bit so like we could just go ahead and give them like um some hover State class name you can say text blue 400 over text blue 500 and save that now we got some links okay so I mean it's not the best footer but it's just something there at least all right let's also do a landing page cuz right now our landing page is just awful so let's just bring in an existing landing page this is typically how I do landing pages um I'll just Google Tailwind landing pages um yeah so I mean like if you have the money I would just buy a license for some type of Tailwind component library but really what we want is just a nice landing page that we can just use I think we only get access to like the first one so like let's just go ahead and grab the first hero section here and uh yeah let's just grab all this and inside of the app technically we could just put a page here I think yeah let's just put a page PSX and then I'm going to paste all this we don't need a dialogue we don't need these hero icons I don't think and I'll just say like landing page we don't need navigation because we already made a header so this header can actually go away and this mobile menu we won't worry about that yet right now now all right let's um let's see how this landing page even looks it might be completely messed up here we go data to enrich your online business so let's find that let's just go ahead and say the coolest way to upload and share files with with your company maybe say easiest and then over here I'll just go ahead and say make an account and start up managing your files in less than a minute cool um let's get this announce our funding because we don't have any funding and then probably we could just put an icon here like our file Drive logo just put it right above that so let's put this here as an image let's just get rabbit from the header so I think we have like that icon here like this Auto Import that go ahead and like make it 200 by 200 and then we could just go ahead and like align this centered somehow I'm not sure why that's not centered doesn't need to be like inline block okay maybe margin bottom eight and I don't know why there's so much padding here like what's going on there um padding y maybe that's it all right so getting started if you click get started we probably want to take it to your dashboard so let's find get started and this could go I make this a next link go to dashboard files Okay click it takes it to your dashboard you can go back um one thing that's a bug is I can't click on anything in the header I think our header is being like obscured by all this other stuff I have to figure out what's going on there I think all we need to do is go to our header maybe give it a relative and maybe get a z of 10 so it's like on top of stuff there we go okay now I can actually do this so I wanted to check if I sign out does uh does it let us go to our dashboard so like let's go back to Locos 3000 um first of all your file shouldn't be showing up so let's find where we're doing your files and this needs to be hidden so the way you can check this is you say con session is equal to use session like that and then um actually no remember we just did a uh we have a signed in component so so signed in and then we can show that that' be an easier way to do it and then now that won't show if you click it started that should take you to your dashboard but then that's going to run a middleware function we're not going to be Lo logged in and it's going to ask you to sign in all right let's just commit this um I think that's kind of all I'm going to be doing in this tutorial I'll say adding landing page my final remarks are I do want to thank my sponsors convex and clerk for making this tutorial happen I hope you guys learned something by using it and be sure to click the links in my description below if you want to check out Clerk or convex like always I have a Discord Channel you guys are welcome to join if you want to find a place to kind of talk to me directly or ask me questions or just be part of a community of other developers other than that I hope you guys enjoyed watching have a good day and happy coding
Info
Channel: Web Dev Cody
Views: 82,953
Rating: undefined out of 5
Keywords: web development, programming, coding, code, learn to code, tutorial, software engineering
Id: 27hMNWcsa-Y
Channel Id: undefined
Length: 277min 21sec (16641 seconds)
Published: Wed Feb 28 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.