Now let's go ahead and let's find a way to actually display this boards which we know that we have inside of our database. So for that I want to go ahead and go inside of the convex folder and create a new API endpoint which is going to be called boards. So multiple. Inside of the board individually we're going to use that to create it, to update it and to delete it. But for the boards we're going to use that for all the querying of multiple boards.
So I want to separate those two. All right, and let's go ahead and let's import the from convex values. And let's go ahead and let's import query from .slash generated server. And let's export const get to be a query which will accept the arguments of for now let's just do an organization id which is going to be a required argument and let's go ahead and give it a handler which is an asynchronous function which has the context and the arguments like that. Now inside of here let's get the identity and let's do await context.out.getUserIdentity.
If we don't have the identity we're gonna throw a new error unauthorized like this and now what we can do is we can fetch all of our boards so we can do that by using const boards to be await context.database.query boards and then we can add with index and you can see in here that we already have one index which comes automatically and we have another one which we've created so if you remember inside of our schema we've created an index by org so we can use this index and then we can use this argument for faster querying we could have of course used the manual equivalent checker but remember that is not an index that will use file sort whereas this will use an index which is much faster for a lot of queries and now let's go ahead and use this now we have access to the query Let's do query equals organization ID arguments dot organization ID and let's go ahead and let's order all of this by descending and let's collect everything. There we go and all we have to do is return the boards like this that is it that is our api route to get all the boards beautiful now let's go ahead inside of the app folder dashboard components board list right here and inside of here now let's go ahead and let me just expand all of this here.
Now we can import use query from convex react and let's also import API from convex generated API and now in here what we can do just make sure this is marked as use client is we can replace this data from being an empty array to instead be use query and pass in api.boards.get and we can pass in organization ID as the argument which we have from the props like that. And in here, I can write if data is undefined, that will represent the loading state. So I'm going to add a, I'm going to open a parenthesis, write a div, loading. So data can never be undefined, regardless if there is an error or if it is empty. If it truly doesn't exist, convex is going to return null for data.
But if it's undefined that means that it's in the loading phase. So you can safely use undefined check to determine whether data is loading or not. Beautiful! And now let's go ahead and let's actually stringify the data itself. And there we go.
You can see how now we have an array and the objects inside of all of our boards which have... You can see how everyone has a different placeholder. Nice! So let's go ahead now and actually try and render them. So for that we're gonna be using a new component called board card but before we do that let's just go ahead and add an h2 element here and let's write team boards and let's write let's give this a class name of text3.xl like that and let's make it dynamic so if query has favorites in that case we're gonna write favorite boards so let me just copy this here so if it has favorite it's gonna be favorite boards like that so now if you switch to favorites it says favorite boards like that great Now inside of here let's open up a div and let's create a grid here.
So grid calls 1 on small devices, grid calls 2 on medium, grid calls 4, on large grid calls 4 again, on extra large grid calls 5, on 2xl grid calls 6, and let's also add Gap 5, Margin Top of 8 and Padding Bottom of 10. And inside of here we're gonna go ahead and iterate over data.map. We're gonna get the individual board and we're gonna go ahead and render a board card element which currently does not exist but we're going to create it in a second. Let's pass in the key to be board underscore ID. Let's pass in the ID to be board underscore ID as well.
We're going to do the same thing with the title. So board dot title. Let's go ahead and pass in the image URL which is going to be board.imageurl let's pass in the author ID which is going to be board.authorID and let's do authorName to be board.authorName And besides that we're also going to pass in the created at prop which is going to be board.underscore creation time and let's also pass in the current organization ID for that board of course. So not the current organization D but for the board created. And lastly we're going to have is favorite which for now we're going to manually write to be false.
Great so now that we have that Let's go ahead and let's actually create our board card element here. So I'm going to create a folder for that because it's going to have some other elements like our sidebar. So I'm going to create a folder board card and inside I'm going to create an index.tsx like that. Let's go ahead and mark this as use client and let's export const board card and let's return a div board card. And now go back to the board list and you can import board card very simply from .slash board card.
So all of this are on the same level. As you can see, board card, board list, all of this are inside of our app folder, dashboard underscore components. And there we go. You can see that we now have board cards in a grid like this. Great!
So now let's go ahead and let's fix this typescript errors which we have right here. So we have to allow all of these types inside of our board card element. So let's go ahead and let's create an interface. Board card props to have an ID which is a string, a title which is a string, author name which is a string, author ID, created at, created at is going to be a number, imageURL which is a string, organizationID which is a string and isFavorite which is going to be a boolean. And now we can go ahead and assign all of those here so board card props and we can safely extract all of those, author ID, author name, created at, image URL, organization ID and his favorite.
Great and now in your board list you should no longer be having any problems with this. Great. Now let's go ahead and let's import our link component here from next link which is going to be wrapping the entire board card. So it's going to be a link and the href is going to go to slash board slash the individual id of the board. Then Inside of here let's create a div with a class name of group.
Let's create an aspect with a custom value of 100 slash 127, border, rounded, large, flex, flex column, so all the items inside are one beneath another, justified between, and overflow hidden. And now let's go ahead and add a div here with a class name of relative flex1 and vgAmber50. That's going to be the background color for all of our cards. As you can see right here, all of our cards have a slight amber color now. And now we're going to go ahead and render the individual placeholders inside using the image component from next slash image so just ensure you've added that and let's go ahead and give them a source of image url let's give it an alt of a doodle and let's go ahead and give it fill property and class name of object bit.
There we go. And now you can see all of our beautiful random images which were created every time we clicked on create a new board. So if you're seeing some empty images, go inside of your database here and just confirm exactly what you're seeing inside of image URL. So you can expand it like this and confirm that this placeholder slash one svg827 or anything that you have you actually have inside of your public folder as we had to do when we added those inside of your public placeholders all of those should exist inside of here. If you don't have them, you're gonna get a broken image picture right here.
Great, so we've added that now. And now instead of having alt of doodle, I think it's better that we actually give the title of the board. I think that makes more sense. Great. And now what I want to do is I want to create an overlay component.
So let's go ahead inside of our board card folder and create a new file overlay.tsx which is going to be a very simple export const overlay and all it's going to do is return a div which can even be a self-closing tag. And let's go ahead and pass in the class name Opacity is 0, group hover Opacity 50, transition opacity Age full, width full and BG black. Like that. And now you can go inside of the index right here for the board card and very simply just below the image add the overlay component from .slash overlay like that. So just make sure you import this which we've just created and now I believe that when we hover it gets a darkened background.
So the reason this works is because we have when we hover on a group we change the opacity to 50 and this has a BG black color, right? And how does the group get hovered? Well, it's because the parent element has the group class name. So everything which is inside of this div, if it has a class name which reacts to the group, it's going to work like this one. Great!
So why do we even do that? Well, we do that because once we hover, we're going to have a little options bar here so that we can rename our board, delete our board or copy the URL for our board. So it's going to be more visible once we hover on it. So it indicates which one is it that we are hovering on and the button itself is going to be more visible. But we're going to do those actions later when we actually create the API routes for them.
What I want to create now is the footer. And before we can build the footer, I want to prepare some labels. And that label is going to be the author label. So to show you who created this board. And the created at label which is going to format the distance from when it was created.
So let's go ahead and install DateFNS for that. So npm install DateFNS. So we can then import the package that we need. I'm gonna go ahead and import format distance to now from DateFNS like that. And in here let's go ahead and let's define const outdoor label is very simply gonna check if the user ID which we can very easily get from use out and you can either import it from clerk next.js or you can import it from convex react but I'm going to be using clerk next.js so you can try out both of those.
So use out clerk next JS, that's gonna give us the user ID. So we're gonna check if the current logged in user ID matches with the author ID of this board. In that case, we're gonna write you, otherwise we're gonna just say author name like that and now let's go ahead and let's write created at label so that's gonna use the format distance to now and it's gonna pass in the created at number and we're gonna add suffix to be true alright so now that we have those two we can go ahead and we can create our footer component so the footer component is gonna go outside of this div which is encapsulating our overlay and our image, but still inside of this last div, so this is what it's going to look like. We're going to pass in his favorite to be his favorite, which for now we are manually controlling with a boolean the title is going to be the title author label is going to be author label which we've just created in a constant created at label is going to be a matching created at label and we're also going to have on click for now to be an empty arrow function and disabled for now is gonna be false like this let's go ahead inside of the board card and create the footer.tsx element like that and let's go ahead and let's import everything we need which is the star icon from Lucid React and we're gonna need the CN library from libutils.
Let's create an interface for their props to accept all of those things so it needs a required title, a required author label, a created ad label, his favorite which is going to be a boolean, onClick which is going to be an arrow function and a disabled prop, which is gonna be a Boolean as well. And now let's export const footer. And let's very simply assign those props. And we can extract the title, outer label, created at label, is favorite, on click and disabled like that. Let's go ahead and let's return a div inside which is for now just going to print out footer.
Now we can go back inside of the board guard and we can import the footer from .slashfooter the same way we did with the overlay and there we go you should just be seeing the text footer at the bottom of each of our cards. And now let's go ahead and actually use that author label and the created that label so it shows us some more information about the board. Let's give this main div a class name of relative background white and padding of 3. Inside of it let's open up a paragraph which is gonna render the title of each of our boards and let's give them a class name of text and let's specifically choose 13 pixels. Let's give it a truncate in case the title is too long and let's give it a maximum width and let's manually calculate 100% minus 20 pixels.
The reason we are calculating 20 pixels outside of the 100% is because this is going to be the space we need to render our favorite button. So we're going to make sure that it never overlays, that the title never overlays with that button and then below that let's add another paragraph which will use the author label comma created at label So it says you about an hour ago like that or whenever you've created yours. Now let's go ahead and style this. So the class name for this paragraph is going to be opacity 0, group hover opacity 100, transition opacity. Text is going to be even smaller 11 pixels, Text is going to be muted foreground and truncate for that text as well in case it's too long.
So as you can see we won't be seeing the information unless we hover on it and then we're gonna see some more information about our boards. Beautiful! And all of your boards should have the untitled title because that's what we that's the way we wrote our code for now. All right and now let's go ahead and let's actually create this little button here. So it's gonna render the star icon which we've imported from Lucid React.
And let's go ahead and give it a disabled or disabled. Let's give it an onclick of onclick for now. And my apologies, we are not doing this to the star we should be doing this for the button like that and let's go ahead and give this a class name to be dynamic using CN. First, let's write some default ones. So opacity is gonna be zero, but when we hover on the main group, the opacity is gonna go to 100.
Let's add transition. Let's add an absolute property. Let's position it in the three points from top. Let's also do three points from the right side. Text muted foreground.
Of course, I'm saying points, but these are actual metrics. So 12 pixels, right? I just wasn't sure which one it was and on hover we're gonna do text blue 600 like that and then we're gonna add a dynamic class name if we are disabled very simply we're gonna do cursor not allowed and we're also gonna do opacity 75. Now inside of the star component itself all we're gonna do is add one more dynamic class name here which is first gonna define the width and the height of the icon and then if we are already favorited this we're gonna add fill blue 600 and text blue 600 as well. Like that.
There we go. So let's try it out now. Now when I hover you can see how my little icon becomes blue when I hover on it. And now let's go ahead and quickly change inside of our index. Is it board card?
Let's go inside of board list right here and change is favorite to true and Now if we coded this correctly this should change to a field icon like this Great. So let's bring it back to false now. Of course later we're gonna make this actually dynamic. And now what I want to do is just give you an ability to add a new card, a new board from this screen because you can see that it is missing right here. So let's go ahead and do that.
Let's head back inside of our board list right here. And just before we iterate over our data and render the board cards, we're gonna go ahead and render a new board button like that. So it's going to be inside of the grid. And let's go ahead and pass in the current organization ID. Like this.
Now Let's go ahead and let's actually create this one inside of the underscore components here. So new board button dot TSX. Let's mark this as use client. And let's export const new board button. And return a div new board button.
Go back inside of the board list and you can then import the new board button from right here. And now you should just see a plain text taking up this space, and we're gonna turn that into a nice blue button. So first things first, let's create an interface new board button props to accept an organization ID, which is a required string, but also an optional disabled boolean. Let's go ahead and assign those newboardButtonProps organization ID and disabled like that. And then inside of here this is not gonna be a div instead it's gonna be a button component so let's give it a disabled of disabled let's go ahead and pass in on click for now to just be an empty arrow function and let's go ahead and give it a class name which is going to be dynamic so prepare the CNUtil from Libyutils so if it is not disabled it's going to have your usual colspan1 property aspect is going to be the same one as from our board card, our board item, so it matches.
And BG is gonna be blue 600, rounded LG. And let's also add while we are here on hover but specifically on this board hover is going to be VGBlue800 so it darkens we already added rounded lg that's good so besides that we're gonna need flex flex call items center justify center and padding on both sides of 6 like this on up and down of six. Great. And now let's go ahead and let's modify this by adding an empty div here. So just so it takes up some space.
That's the only reason I want this to take up some space and then add a plus icon from Lucid React. So input a plus from Lucid React like that. And let's give this a class name of H12, width of 12, text white, and let's make it just a bit tinier by giving it a stroke of one, like that. And now let's add a paragraph new board. And inside of here let's add a class name text small, text white and font light there we go, so now we have this setup right here And I forgot to add the actual dynamic class here.
So if we are disabled in that case, opacity is going to be 75%. Like that. So let's try it out. New board like this. There we go.
I think this looks fine. Let's just see. The text looks a bit big in my opinion. Text small, so this should be enough. I don't know, you choose if you want extra small or text small.
Right. Alright, and now what I want to do is I want to actually create the function so it creates the new board when we click on that and for that there are two ways we can do that if you remember so you can either use create using use mutation directly from convex react like that and then pass in the api from convex generated api so api.board.create And then you can create on click here. And very simply in create, you can just pass in the organization ID and the title untitled like that. And then you can pass the on click right here. So if you try that out, it should already be working.
So if I click new board, there we go. New one is created. But I want some loading states and I want to use my use API mutation. So I'm going to change this to use use API mutation from my hooks use API mutation and I no longer need this one like that and let's go ahead and pass in api board create and let's execute mutate and pending so this is gonna be mutate like that I'm gonna change this to use pending or disabled and this thing as well pending or disabled And let's see if there are any more places. I don't think there are any more places where this is needed.
Great. And now Let's go ahead and try it out. So if I go ahead and click again, there we go, you can see how it's disabled for a quick second. And now let's just add the Toast notifications. So Toast from Sonar.
So after we successfully mutate, we're going to go ahead and get the ID of the new board. And then we're going to add those.success, board created, and I'm going to add a todo, redirect to slash board ID. And let's add .catch, which is very simply going to go to the error and say failed to create board and we can already try that out so when I click here there we go board created this is loading nicely very very nice so now let's go ahead and let's actually create a proper loading element for our items right here So for that we have to quickly go back inside of our board card component inside of index right here and we also have to add the skeleton component so let's go inside of terminal here and let's run npx chat-cmui at latest add skeleton so skeleton like this and then let's go ahead and let's actually import the skeleton So import skeleton from components UI skeleton. And in here, we're going to write board card dot skeleton is going to be a function board card skeleton. And let's go ahead and let's return a div which is going to be exactly the same as this one so you can actually copy this div because we are trying to replicate what's actually going on but we actually don't need a border itself we do need a roundedLG.
We don't need flex or flexCol. We do need overflowHidden and we also don't need group. And then we can just render the skeleton inside and make sure it fills 100% of this space. There we go and now we can go back inside of our board list And then instead of just rendering this loading right here let's go ahead and make this a bit easier for us to see. So I'm gonna change this manually to always be true so that we can always see the loading text in here.
So now we're gonna change that to actually display our elements. So for that I want to actually copy this first two elements. First three actually. So I copied the div, I copied the h2, and I copied our grid. So let's just make sure we have all of those like this.
So you should have the div, you should have the H2, which can just say, well, it can be, it can work like this query.favorites, favorite boards or team boards. And then this div is just an grid and inside we are going to render the new board button but we're gonna make it disabled so pass in the organization id and also pass in disabled and then in here we're gonna pass the board card the skeleton like that And copy it four times or however many you see fit. And there we go. This is going to be our loading screen. You can see how it kind of shimmers.
You can see how this button is disabled. Oh yeah, and also when a button is disabled perhaps we should not change its color. So let's go inside of new board button and we're also gonna add on here hover VG blue the default one. Like that And let's add cursor not allowed also. Like that.
Great. So let's try it out now. There we go. So now it doesn't change color. So this looks like a very nice loading skeleton right now.
Great. And I think the same will be true in our favorite boards. Great! So let's go back inside of board list now and let's change this to be only if data is undefined. So only while it's loading is that screen going to appear.
So when you refresh if you have a bit of a slower connection you can see how it actually displays that skeleton for a second. Perfect! So we just created a nice way to render all of our boards. What we're gonna do next is we're gonna add the functionality to load well first we're gonna actually do all the actions right to rename them to delete them and to add them to our favorites and only when we add them to our favorites We're gonna go ahead and figure out the favorite board tab and how we're going to render only those. And then we're gonna wrap it up with the search, which is gonna be very, very simple.
We can do it already, but I just want to go ahead and finish the actions for our cards. Great, great job!