So now let's go ahead and let's create the functionality to create cards inside of lists. So for that I want to go back inside of the ListItem component. So inside of this underscore components in the board.id folder find the ListItem. And inside of here first thing I want to do is I want to define the is editing and set is editing to be from use state with a default value of false and make sure you import the use state from react Then let's go ahead and let's add const textAreaForm sorry, ref to be useRef from react as well as elementRef from React and give it a type of text area. And let's give it a default value of null.
So just make sure you have elementRef and use refImported and let me just separate those two. Like that and now let's create our functions. So const disableEditing is very simply gonna turn the set is editing to false and now let's go ahead and define const enable editing which is going to be set is editing true and also set timeout here and now let's just focus on the text area ref, so text area ref current focus like this. Great and now inside of our list header we can now add onAddCard here to be enable editing. Because remember we have another action inside of our list options which is used to add a new card.
So that's why we are defining this states inside of the list item rather than a new component which we are going to create here which will be called card form. So now we have to add this props inside of the list header. So let's go ahead and define onAddCard to be a void like this. And then let's use that prop. Let's also extract it here from the props.
Go alongside data. And let's find the list options where we, if I remember correctly, passed an empty function for add a card. Well, now it's going to be add a card. There we go. So we can go back inside of the list item now.
And now outside... Sorry, inside of this div, just below the list header, let's add a card form, which we don't yet have. So when we save, we're going to get an error. And let's already prepare here by giving it a ref of text area ref Let's give it isEditing prop to be isEditing, enableEditing to be enableEditing, disableEditing to be disableEditing. And let's also pass in listId, which is going to be data.id.
Like that. Great. And now let's go inside of here and create a new component card-form.psx. Let's mark it as useClient and export const cardForm and return a div cardForm. And now let's just quickly create the interface card form props which is going to accept the list ID which is a string, enable editing which is just a void, disable editing which is exactly the same and is editing which is a boolean.
And let's go ahead and assign those props so card form props and let's extract list ID, enable editing, disable editing and is anything here. Let's go back inside of the list item and now we can import card form from .slash card form the same way we did with a list header here. And you should no longer be having any errors and here underneath the header you should now have a text which says cardform. So let's go ahead and modify the cardform so it actually looks like something. And one thing that we forgot to do is the ref forwarding here.
So let's go ahead and change this a bit. So I'm going to remove this prop assignment here and instead I'm going to mark this entire thing as forward ref which we can import from react and let's open another brackets here and let's end the brackets here at the bottom and then let's open pointy brackets for the forward ref and let's give it HTML text area element and comma let's give them card form props and now let's go ahead and inside of this well to get rid of these errors first thing let's do is card form dot display name to be card form like that and now the alongside this props here add a comma and exclude the ref itself. Like that. Perfect. So now, what I want to do is render a button here to say add a card.
So let's give this div a class name of PaddingTop and PayX2. Let's open a button from add slash components UI button. So let me just separate those imports here. Inside of the button, let's write add a card. Let's go ahead and close the button and let's also add a plus icon from Lucid React so make sure you have the plus icon from Lucid React Let's give this plus icon a class name of H-4W-4 and margin right of 2.
Let's give this button on click to be enable editing. Let's give it a class name of H-autoPX2PY1.5WFullJustifyStartTextMutedForegroundTextSmall. X2, py, 1.5, w, full, justify, start, text, muted, foreground, and text, small. Let's give it a size of small and a variant of ghost. Like that.
And there we go. Now we have a nice little button here which says add a card and it actually switches the state from is editing false to is editing true but right now we haven't created any functionality when that happens so what I want to do Next is I want to create a reusable component called a form text area. So let's go ahead and do that. I'm gonna go inside, I'm gonna close everything in fact and I will go inside of my components folder. Let me just find it here and create a new file.
Actually, let's go inside of the form and create form-textarea.bsx. Let's mark this as useClient and let's go ahead and write an interface form-textarea-props, which is an ID, an optional label, a placeholder, a required prop, a disabled prop, errors which is going to be a record, string, string array or undefined. Let's go ahead and give it a class name which is a string. On blur which is going to be an optional void and on click which is going to be an optional void as well and on key down which is going to be an optional keyboard event handler from React so make sure you import that which is going to work on HTML text area element or undefined and let's give it a default value which is an optional string so a lot of props for this one and now let's go ahead and write export const form text area to be forward ref which you can import from react as well and let's immediately add form text and let's actually call it form text area with the lowercase a like this so form text area dot display name is going to be form text area.
Now let's go inside the forward ref let's go ahead and open parenthesis and let's go here and let's give it an HTML text area element and form text area props like that. So basically this two, let me go ahead and try and collapse those or perhaps I can do it. So basically these two elements HTML text area element and form text area props which we created. Now go ahead and open parentheses again and immediately let's go ahead and destructure the ID label placeholder required disabled errors on blur on click and let's not make a typo in the on blur here besides on click we're gonna have on key down last name and default value Now outside of this props let's extract the ref and then before we end this parentheses open an arrow function like this and go ahead and return a div and that should get rid of your errors. So make sure you do forward ref, assign the proper type to it, open parenthesis and open another parenthesis which represent the props and inside you can immediately destructure the ID, label, placeholder, all of that stuff and after that add a comma and add a ref and then open an arrow function and just make sure you have an additional parenthesis here to close the forward ref which we opened.
Great! Let's give this div a class name of space y2 and w-full and now let's add a new div with a class name space Y1 and W-full as well and inside if we have the label we're gonna go ahead and render the label from slash UI label and I'm just gonna replace that to use slash components. Let me separate it alright let's go ahead and close the label here and inside let's render the label like this and Let's also give this an else to be null. Let's go ahead and give this label an HTML4 element to be ID, last name to be text, extra small font, semi bold, and text neutral 700, like that. And now let's go ahead inside of the ShedCN, sorry inside of the terminal and I'm just gonna go ahead and add npx ShedCN UI latest add text area like this Let's wait a second for this to install.
Perfect. And now just below this label here, go ahead and enter the text area from slash UI text area, which again, I'm going to replace to use add slash components like this. And inside of this text area, let's go ahead and assign all the props we need. So on key down is going to be on key down, on blur is going to be on blur, on Click is going to be on click. Ref is going to be ref, which we forward.
Then we're going to have required prop, which is required. We're going to have the placeholder, which is placeholder. We're going to have name, which is ID. We're going to have ID, which is ID. We're going to have the disabled prop, which is going to be the disabled prop.
And we're going to have class name, which is going to be dynamics. So go ahead and import CN from at slash lib slash utils and inside of here go ahead and open parentheses And first let's write the default classes which are going to be resize-none, focus-visible, ring-zero, focus-visible, ring-offset-zero again. Then we're going to have a separate ring-zero, focus-ring-zero and outline-none and we're going to have shadow small like that. And now add a comma and paste in the class name prop like that. And let's also add area-describedby to be open backticks the id dash error so we have the accessibility setup and let's give it a default value of default value Perfect and outside of this div go ahead and render the form errors which you can import from .formerrors so we already have that component like this and let's go ahead and let's pass in the id to be id and errors to be errors.
Great! And now one more thing that I want to do is I want to use the use form status hook so it's disabled while the form is pending so extract pending from use form status which you can import from React DOM. Let me just move it here to the top like that. And then let's use this pending here in the disabled prop. So we're going to use either pending or disabled like that.
Perfect. So now that we have our form text area set up we can head back inside of our CardForm component which we started creating where we added this Add a Card button and a plus sign so that's again located in app, platform, dashboard, board, board ID, components, card-form right here so now let's go ahead and modify this so that if is editing in that case go ahead and return a form element which is going to have a class name of m-1, m-1, pa-0.5, px-1 and space y for and inside render the form text area from add slash components form text area which we just created and let's go ahead and let's give it an ID of title let's give it an on key down for now to just be an empty arrow function. Let's give it a ref of ref which we are forwarding. Let's give it a placeholder of enter a title for this card. And let's go ahead and let's give it a class name.
Well we actually I don't think we need to do this class name instead let's just pass in the errors to be errors and we don't have the errors yet so how about we don't give them just yet and looks like we have a missing on click property. So let's go back inside the form text area and let's just ensure that on click is optional. So just add a little question mark here, my apologies. And there we go, now you should have no errors here. Perfect.
Below this form text area add an input which is going to be hidden. The id of this input is going to be a list id, the name is going to be list id and the value is going to be list id which we have in the props here and now what I want to do is go below this input and open up a div with the class name of flex items center and gap x1 and let's go ahead and add a form submit option which you can import from s slash components form form submit as I did right here and let's write add card and below that go ahead and add a button component which we already have imported if I'm not mistaken. That's right, we do. Alright. And inside of this button component render the X from Lucid React.
So just make sure you have X alongside plus in your Lucid React imports here. And give it a class name of H-5 and W-5. And go ahead and give this button a non-click, disable editing, a size of small and a variant of ghost, like this. And let's see what we come up with here. So now when I click on add a card, there we go.
I should have a beautiful text area here, which says enter a title for this card and we can close it from here as well. And when we open it is immediately focused. Perfect! So what we have to do now is we have to create an action which is going to create the card. So as always let's copy an existing server action for this.
So for example let's do create list and let's paste it here. Let's rename it to create card. Let's go inside of the schema first and let's rename the schema to create a card like this. And what we expect inside of the create card here is the title which can well the rules can stay the same we expect the board id but we also expect the list id like this Now let's go inside of types.ds and let's change this to createCard from the schema and let's assign that to our input type and our return type should work with card now so replace that as well and then go inside of index.ds and let's do the same thing so create card from the schema and we scroll all the way down create safe action now uses create card and the function name should be create card great and now inside this handler let's see what we have to do. So the authorization can stay the same.
From the data we destructure the title, the board ID, but also the list ID. And we are no longer working with the list instead we are working with the card. So inside of the try block let's go ahead and remove everything here. So make sure the try block is empty and let's go ahead and first let's attempt to fetch the list where we are trying to insert this card. So const list is await db list find unique where id is list id and board uses the same organization id as the currently logged in user.
If there is no list return an error which says list not found. Great, so that is our protection and now let's go ahead and let's get the last card inside of this list so we know what order to give this new card. Even if it is the first one we need to do that check so const lastCard await dbCardFindFirst where we have a matching list ID and orderByOrderDescending and order by order descending and select order true and now let's define the new order to be last card, if we have done, it's going to be last card order plus one otherwise it's the first card so we can just put the order to be one and now finally let's define that the card is await DB hard create Let's give it the data in which we will define the title, the list ID and the order to be new order. Perfect, so that is it. The error can stay the same.
The revalidation is correct and the return returns the card and not the list and that should be it. Perfect we can now go back inside of our card form component so inside of the board id underscore components we have card form it's alongside all of those list header, list items, all of those stuff we just worked on. So we just finished this button which triggers the is editing and changes it to a form. So now let's go ahead and let's import everything we need to trigger the action. So we need the use action hook from hooks use action and we need the create card from actions create card and let's see if that's going to be it.
I would also like to import use ref from react as well as element ref and let's also import keyboard event handler like that great and I already started this react so yeah let's not have two imports let's just move the forward ref to be here so no need to have multiple imports of exactly the same thing in fact I think it will throw an error okay and I think this should be enough for now now let's go ahead and just also add useParams from next slash navigation like that and let's go ahead here inside of the card form and let's add const params to be used params which we imported const form ref to be use ref element ref of form by default it's going to be null and let's write const use action create a card and let's go ahead and execute and extract execute and field errors. Great! And now let's write const on key down which is event keyboard event if event.key is escape in that case we're going to disable editing. Alright, so we have that done. Now let's go ahead and let's add the useOnClickOutside which you can import from useHooks.ts.
I'm just going to move it right here. So import use on click outside and also import use event listener here. So we're just doing some user experience features now, right when they click escape, when they blur, when they click enter, when they click escape, all of those things. So inside of the use on click outside we're gonna pass in the form ref and we're going to trigger disable editing. Then we're gonna write use event listener here which we imported to listen to key down listener and then we're gonna trigger the on key down.
And now let's create const on text area key down to be a type of keyboard event handler which handles the HTML text area element. It accepts the event prop and open an arrow function and let's check if event.key is enter and if we did not press the shift key at the same time in that case prevent default and instead let's go ahead and request submit for the form So we're going to consider if a user presses enter inside of the text area, which by default just opens a new line, we're going to submit this form because that's what Trello does, except if they are holding shift, in that case, they're going to go into a new line and finally let's create const on submit to be form data which is a type of form data. Let's extract the title to be form data get title as string. Let's copy this two times. Let's replace the second one to be a list ID and the third one to be board ID.
Let's go ahead and let's call the execute prop and let's give it a title list ID and board ID. Perfect. Let's go ahead and let's assign the ref to this form to be form ref and let's give it an action on submit. Great! And now we also have to pass this onTextArea key down inside of the form text area.
So go inside of the ifIsEditing clause, find the form text area and replace this empty arrow function with onTextArea key down and let's also give it errors to be field errors which we now have because we added this function execute and field errors and now let's go ahead and let's import toast from sonar package and let's go ahead and add some callbacks so on success is gonna get the data and we can call toast.success openvectics card open annotations success, open backticks, card, open annotations, data.title created like this and let's do form ref.current reset so it clears the form And let's add on error here to get the error and calls toast error with the contents of the error. Perfect! So I think this should be it now. Since we don't have any UI to display the cards I'm gonna go ahead and run npx prisma studio here so it opens on localhost 5555. So let me just prepare that here I'm gonna refresh this I'm gonna paste the prisma studio here and let me focus on the card collection here Right now as you can see there are no rows inside of my table.
So I have opened the card rows and let's go ahead and try this out. So I'm gonna go and write test. I'm gonna click add and it looks like something is not working as it should. Let's go ahead and debug why. So if I...
Let's see, already when I open there seems to be an error. All right, let's see why it is not working. So first thing I'm gonna do is I'm gonna confirm that this onSubmit is actually having all the values. So title, list ID and board ID. So let's try that out now.
So I'm gonna open the console here and let me just focus. All right. Let's write test here. And when we click test, oh, the board ID is null. Oh, all right.
So we forgot to pass the board ID. That's why the function is not working here. So let's go ahead and let's pass the board ID. So we have to do that, I believe. I believe we can actually do it inside of the onSubmit here because yeah we can use the params here so we added this useParams but we're not using it but we need it to extract the current board id So we can either add another hidden input field or we can use the params.boardId here directly and also write it as a string like this and let's see if that's going to improve this when I write test here and click add a card, there we go card test has been created.
Let's try one more time and there we go the form is cleared and I have a notification that card has been created. Let's check my Prisma Studio now and I should have two records and I do and they have the correct order. They have no description and they have a relation with the list. Great, great job! So you finished creating cards.
What we have to do next is we have to iterate over the cards and actually show them in individual list. So rendering the cards is actually quite simple because we already have them. Because if you remember our page.tsx we also include the cards when we load the lists. So inside of the list container here, when we pass the data to the list item, we already have access to all the cards. So let's go just above this card form here and let's add an ordered list element.
So this is the list item component. Make sure you are in list item here. And just below the list header and above the card form go ahead and add an ordered list. And let's give it a class name which is going to be dynamic so import cn from at slash lib slash utils and let's give it some default classes which are going to be mx1 px1 py 0.5 flex flex dash col and gap y2 And then add a comma and let's add a dynamic class which is going to be if data.cards.length is 0 in that case is larger than 0, in that case we're going to add a little space from the top otherwise we're not gonna add any space from the top perfect and now let's go inside of this ordered list and let's do data.cards.map let's get the individual card and the index and let's go ahead and render the card item component which we don't yet have but we're gonna create it in a second. Let's pass in the index which is an index, key which is card.id and data which is the card itself.
Perfect. So inside of this very same so let me close everything but this. So inside of this folder where we have list header list item basically everything and our newly created card form now let's create carditem.vsx Let's go ahead and let's mark this as useClient and let's export const cardItem here and return a div cardItem. And now let's just quickly create an interface card item props to accept the data which is the card from Prisma Client and an index which is a number and now we can go ahead and destructure this props and add them here so data and index like that and let's go back inside of our list item and now we can import the card item from .slash card item as I did right here. And when you save you should be having no errors.
And now let's just go ahead and style this. So instead of rendering card item we're going to render data.title. And let's go ahead and give this a class name of truncate border2 border transparent hover border black py2 px3 text small bg white rounded MD and shadow SM and if I take a look now there we go we have our cards here perfect and if I create a new one here there we go It is immediately visible in my list. Perfect. What we have to do next is finally implement our drag and drop.
So we're going to do that before we do opening a card in a model, right? We're going to do that later. And just one more thing that I want to do before we wrap up is give this div a role of a button. Like that. So now when you try you can see that you have a clickable element and you're going to be able to drag it or you're going to be able to click on it.
Right now none of those works but what's cool is that we revalidate this path and we do router.refresh so the server components are all up to date and we have the newest data here. Perfect! Great, great job!