So now that we've wrapped up our drag and drop which is working flawlessly let's go ahead and implement the functionality that when we click on a card we open a model which then displays the content of the card and from then we can rename it, we can add a description and we can also do some actions here. So let's go ahead inside of our hooks folder here and I want to copy and paste the use mobile sidebar and let's rename it to use card model. So I copied the use mobile sidebar and rename it to this And let's change this mobile sidebar to be CardModel like this. So we have the CardModelStore, we have the UseCardModel and CardModelStore here in the CreateTypes. And I want to modify it a bit by adding an optional ID here and whenever we open it we're going to pass in the ID of the card we want to open and then let's explicitly add the ID to be undefined in the initial state and in the in on open let's extract the ID and let's go ahead and set the ID and when we close let's reset the ID to be undefined.
Great and now what I want to do is I want to actually create the card model. In order to do that we're going to need to have the dialog components from ChatCN. So let's go ahead and run npx chatcn ui latest add dialog like this and let's wait a second for all of this to install and let's do npm run dev again and just refresh the localhost if you shut down your app. Now let's go inside of the component and create a new folder called models and inside let's go ahead and create a new folder card model and inside create an index.tsx because we're going to have multiple components inside of this model, of course. So let's go ahead and let's mark this as useClient and let's export const cardModel here and what I want to do is I want to return a dialog from components UI dialog so just make sure you don't accidentally use the from the radix and then let's render the dialog content from components UI dialog and inside I'm just gonna say I am a model like this and let me just add the semicolons here and now let's connect it to that useCardModel hook so const useCardModel is useCardModel and let's actually change this to be CardModel and now let's go ahead actually let's not do it like this let's individually get each one so let's do the id useCardModel state and we pick the state.id Let's get the isOpen to be useCardModel, get the state and do state isOpen.
And let's do const onClose to be used card model get the state and state onClose and now we can apply those props to the dialog itself so open is isOpen and onOpenChange is going to trigger onClose like this. Now let's create a model provider. So inside of the components folder create a new folder called providers like that and inside create a new file model-provider.tsx. Let's mark it as use client and let's export const model-provider here and let's return a fragment and inside we're going to add our card model from models card model like this and I'm just gonna change this to use components and now I want to go ahead and protect it from hydration errors so let's give this a use state with the default value of false and let's go ahead and extract isMounted and setIsMounted and then in here I'm calling a useEffect which is going to set the isMounted to true and then I'm adding an if clause that if we are not mounted we just return null. So this ensures that this component is and everything inside is only rendered on the client because useEffect can only be rendered on the client so unless this isMounted has been turned to true by the initial mount here it will not be rendered and thus it will not be creating any inconsistencies when it comes to server and the client thus preventing hydration errors.
Now let's assign this model provider to our platform layout. So go in the app folder platform layout.tsx and just below the toaster go ahead and add the model provider from components providers model provider let me just align my imports a bit there we go so just make sure you have that Now we have to go back inside of CardItemComponent. So inside of Platform, Dashboard, Board, BoardIDComponents, you have a little CardItemComponent. And let's go ahead and use our CardModel hook. So const CardModel is UseCardModel.
And we can import useCardModel from add hooks useCardModel and let's go ahead and give this button an onClick which is an arrow function which calls card model on open and passes in the data ID so we know exactly which well card to load. So when I click on the one now there we go you can see how we have a nice little model here. Perfect. But it seems like this background is just a bit too light for me. So what I want to do is I want to go inside of the dialog itself.
Let me close everything and let's go inside of components UI dialog right here. And I want to find the dialog overlay right here and in here we use BG background 80. I want to use BG black like that and I think that should look just a bit better when I click on something. There we go it has a black background now and our model is opened. Perfect.
And now what I want to create is a component which is going to display the title of the model of the card. But before we can actually display any information, we have to fetch the information from somewhere. Up until now, we only fetched information using the server component and directly accessing the database. But due to the nature of this drag and drop component, which requires the items to be client components, we're going to have to create an API route. And to call that API route, we're going to be using React 10 stack query.
So let's go ahead and let me just close everything here and let's go inside of our terminal and let's do npm install at tensestack-react-query like this. Let's wait for a second for this to install. Let's do npm run dev again and let's go back inside of our providers in our global components here and create a new file queryProvider.tsx. Let's mark this as useClient and let's go ahead and import use state from react and let's import query client and let's import query client provider from at down stack react query and then export const query provider which has children and go ahead and give the children a type of react.reactNode and inside of here, let's assign the query client in a state and let's make it be the new query client. So let's just the new query client and then let's just return the query client provider, let's render the children inside and let's pass in the client to be query client, like that.
Now, let's assign this new query provider back inside of our platform layout. So inside of the app folder we have the platform and layout right here. And let's go ahead and let's wrap everything inside of our query provider from components providers query provider so make sure you do it from here and including the children as well so what this this is not going to turn everything inside into client components just because we've wrapped this as client because there is a way to render server components like a bunch of our children are inside of client components and you do that by passing them as children and that is not going to change the fact that the children are server components just in case you had that doubt. So let's just refresh the localhost here to confirm that all of this is still working to confirm that our server components are still server components and as you can see they are because this was loaded directly from the database perfect and now what I want to do is I want to create an API route to fetch a specific card. So let's go ahead and let's go inside of the app folder and create a new folder API and then inside create another folder called cards and inside of that create a new folder with a dynamic ID card ID and instead of page since this is an API we create route.ds.
So now let's go ahead and let's export asynchronous function get Let's get the request which is a type of request and we can destructure the params. So let's give them a type of params and get the card ID which is a string. Just confirm that your card ID exactly matches the folder name card ID otherwise it's not going to be visible here. Go ahead and open a try and catch block and we can resolve the error first. So let's just do return new next response from next slash server and let's pass in internal error and a status of 500.
Inside of the try block first let's attempt to get the user ID and the organization ID from the out Handler. So make sure you import clerk from Next.js. If we don't have user ID or if we don't have organization ID you can return new next response unauthorized with a status of 401. And now let's go ahead and let's import our database from at slash lib DB and in here let's go and fetch the card. So const card is awaited DB card find unique where ID is params card ID list has a board which has the matching organization ID so only members of the board can access this and let's include a list but the only thing we're going to need from the list where this card is is the title so write select title true so no need for any additional information and now let's just write return next response dot JSON card.
Great! So now we have our API request ready. Now let's create our reusable fetcher. So I'm gonna close everything, go inside of the lib folder and create a new file Fetcher.ts and in here export const Fetcher which takes in the URL which is a string, uses the native fetch and passes in the URL and then transforms the result to JSON. So it's readable for us like this in one line.
Great and now we can go back inside of our components models card model right here and let's go ahead and let's import the use query from tanstack query so import use query from tanstack query like this and let's write const use query and let's define what we expect from it which is card with list which we have defined in our at slash types so let me show you that you should have card with list which is a card from Prisma Client and it includes a list. And let's go ahead now and let's actually define the query key to be card and ID and let's do the query function to be a void which calls our fetcher from at slash lib fetcher and passes in backtick slash API slash cards and then pass in the individual ID like that and then inside of your const you're going to have data which we're going to save as card data like this. Perfect! And now let's go ahead inside of here and let's render data, sorry card data question mark title like this and let's try it out. So I'm going to refresh this and when I click on new card there we go it says new card if I click on one two three it shows one two three And you can debug this if something's not working by going inside of your network tab here.
And let's go to slash cards. Let's try again. Alright, and you just type in card here and let's click on a card and there we go you can see the exact ID that I'm passing and the exact result that I received back. Perfect so Now we can go ahead and we can finally create our header component from here. So let's go ahead and do that.
So I want to go and open the browser here where I have my card model and inside I want to create a new file which is going to be named header.dsx and let's go ahead and mark this as use client and let's export const header and let's return a div saying header. Let's go back inside of the index component here and what I want to do is I want to render the header component and I want to pass in the data to be card data and let's import the header component from dot slash header so this one right here and let me just join this imports because they are of the same nature like that and now let's create the types for the header so it can accept this data. So inside of the header create an interface header props which accepts the data which is carved with list which we have inside of our types. Let's go ahead and give them header props here and destructure the data like that. Perfect!
So what I want to do now is I want to go ahead and render this but I kind of want it to be an input by default So we're going to see what I have in mind. So let's give it a class name of flex-items-start-gap-x3-margin-bottom-of-6-w-full. And now I want to go ahead and add a layout icon from Lucid React. So just make sure you import that and it is a self-closing tag and let's go ahead and give it a class name of h-5w-5 margin top 1 and text neutral 700. And now let's create a div here with a class name of WFull and let's open up a native form element and let's render the form input from components form form input like this and inside of it we have to give it an id of title we have to give it a ref of input ref which we don't yet have so let's not do that just yet and let's go ahead and give it...
Well, first thing I want to do is I want to store the title inside of useState here. So give it the data.title like that. And make sure you import useState from react because we're going to do some optimistic update on that as well. So now we can give this a default value of the title from the state and let's go ahead and give it a class name of font semi-bold text extra large px1 text neutral 700, bg transparent, border transparent, relative, minus left, minus 1.5, width of 95% and on focus visible I wanted to have a background of white, on focus visible I also wanted to have a border input, a margin bottom of 0.5 and truncate like that. Perfect.
So now as you can see when I click on a specific card, so let's just refresh so we ignore this. Okay, let's try it like this. Let's use data question mark title like that or I actually think now yeah now nothing's going to be displayed here all right let's let's just continue developing and we will see the results in a moment So in order to fix that little bug, let's go ahead and let's create a skeleton here. So header.skeleton is a function, header skeleton, which returns a div with a class name of flex-items-start, gap-axe-3 and margin-bottom of 6. And inside we render the skeleton which we can import from add-slash-components-ui-skeleton as I did right here.
And let's go ahead and let's give this skeleton a class name of H-6W-6 margin top 1 and BG neutral 200 like that and then below that let's open up a div and inside let's render another skeleton let's give it a class name of h-24 actually width of 24, height of 6, margin bottom of 1 and BG neutral 200. Copy and paste this below and let's give this one a width of 12, a height of 4 and it doesn't need any margin bottom. And now we can use this header skeleton here. So go ahead here and remove the question mark for data.title. It's always going to have it.
And now go back to the index where we have an error because the data is possibly undefined. So let's go ahead and do the following. If we don't have card data in that case we're gonna go ahead and render header.skeleton like that. Otherwise we're gonna go ahead and render the actual header like this. So let's try that out.
Now I'm gonna refresh here and when I click you can see how for a second I have a nice little loading skeleton and then it loads the exact card which I expect. Perfect. Let's head back inside of our header component and let's give it all the necessary stuff it needs to successfully update this. So I'm going to go ahead and I'm going to add the query client from use query client which we are going to use to refresh the data once we successfully update it using the server action. So let me move that to the global imports at top.
We're also gonna need the params. So use params, which we can get from next navigation as I did right here. Besides this, we're also gonna need to create a ref. So let's write const input ref to be useRef from react get the element ref from react as well and give it a type of input and inside give it a default value of null. So just make sure that from react you have the element ref, use ref and use state.
Great, so now we have those and let's go ahead and just create one more which is going to be on blur. So const on blur is going to call the input ref.current.formRequestSubmit Alright, and now let's go ahead and let's give this form input what it needs so ref is going to be input ref on blur is going to be on blur and we already gave it the default value so that's good and now let's just go ahead and let's create const on submit here with form data which is a type of form data and let's console.log formdata.getTitle. So now and of course let's assign this to be the action of this form. So if we try this out already, I'm going to open my inspect element here and if I try and change this to something and press enter, there we go, you can see the exact form field which is going to be submitted to the server action which we are yet to create. Before we do that let's go to the end of this form function and let's create a paragraph which is going to render in list open a span element data list title like this and let's go ahead and give this paragraph a class name of text small and text muted foreground and let's give this span element a class name of underlined like that.
Let's try it out now. I want to refresh and click here. There we go. You can see it says new card and then where my list is at and you can see how flush this input is. It's barely noticeable.
There are almost no flickering and stuff when you click and when you blur. Perfect. So now let's go ahead and let's actually create that server action. So let's copy an existing server action. I'm gonna go ahead and find update board, that's the closest one and let's rename it to update card like that.
Let's go inside of schema so we tell it exactly what we expect. So let's change this to be update card and alongside title we're also going to need a board ID which is going to be z.string we're also going to need a description which is z.optional, z.string and then we're going to go ahead and give it a required error of description is required we're gonna give it an invalid type error of description is required as well and let's also give it a minimum value of 3 just so we can see those errors and a message of description is too short. All right and so we have the board ID, the description, the title and we have the ID of the card we are trying to update. Perfect. Now let's go inside of the types and let's get the update card from here and let's add it to the typeof.
Great. And now let's go ahead inside of the index. Let's change the schema to use the update card. Let's go all the way down and add the update card and change the function name to update card as well and now inside we have to modify the handler So this can stay the same and in here we're gonna be extracting the following. So we will extract the ID, the board ID and the rest of the values we're gonna keep in a constant values which we will spread like this and we're not gonna be working with a board but with a card so change this left to be a card and then in here let's go ahead and let's simply change the card to be a WaitDBCard.Update where ID matches the ID and the list has a board which has the matching organization ID and then in the title we're just gonna go ahead and spread the values like that.
Perfect! And we can leave this to be failed to update and let's use the board ID here and in the data let's return the card and it seems like I forgot to update my types here yes, so go back inside of the types and change from using the board to using an individual card as our return type. Great! And now let's go back inside of the header component inside of models card model so go inside of header here and let's go ahead and let's import the use action from hooks use action and let's import update card from actions update card and now let's go ahead and actually well create this hook right So right here I'm going to call the execute and I'm going to call the use action I'm passing create, sorry update card here like that and then inside of this on submit let's go ahead and change this to be a constant title and let's do execute and let's pass in the title but I believe that it also needs a couple of more things here. So let's fetch the board ID to be params board ID as string.
Remember, we have the params from use params here. So make sure you have that. All right, and let's pass in the board ID and the ID is going to be data.id. Like that. Perfect.
And let's change this to be as string and let's also prevent updates if the title matches the current data.title so we can then just break the function instead. Perfect! So that should now be working. How about we try it out? One more thing that I just want to do is add the callbacks, right?
So first let me just go ahead and let's import the toast from Sonar and then inside of the use action let's go ahead and add on success. Let's get the data from here and first thing I want to do is re-invalidate the query. So queryClient.invalidateQueries and let's go ahead and pass in the query key to be card and data.id like that. And then let's call toast.success and open backticks, renamed to open annotations and render data.title. And set title to be the new data.title.
And let's add on error here, error. And inside, well, we can just call the toast.error and pass in the error. So let's try it out now. I'm gonna go here, choose a card, change the name, press enter and there we go. Renamed to change the name and it's immediately reflected here and I can also click just outside and then it does the exact same thing.
Perfect! So now we're gonna go ahead and create some additional actions like changing the description, copying the card or deleting the card and then we're gonna go ahead and learn how to create activity logs.