So now I want to create the description component. So let's go ahead and just prepare for that. It's going to be very similar as our header component is. And the first thing I want to do of course is just create a little skeleton for it so we can render it conditionally. So go inside of the card model and create a new file description.psx Let's go ahead and mark this as use client.
Let's export const description. We can immediately create an interface for it. Description props. And you already know that we accept the data, which is a type of card with list. Now let's go ahead and assign those props here.
Description props. And let's destructure the data. And for now, let's just return a div, rendering data title. Or perhaps it would be smarter to render the description, even though we don't actually have the description since it is optional so this will be the first time the user can actually add a description and now let's go ahead and let's just create a proper skeleton for this one so I'm gonna write description.skeleton is equal to a function which is a description skeleton and let's go ahead and return a div. Let's give this div a class name of flex items start gap x3 and w pull.
Inside of this div let's render our skeleton from add slash components ui skeleton. I will just align it like that. Let's give this a class name of h-6w-6bg-neutral200 And now let's create another div here with the class name of W-full. Let's go ahead and copy the skeleton and put it inside here. And let's modify its class name a bit.
So I'm going to leave the BG neutral 200 but I'm going to add the width to be 24, the height to be 6 and margin bottom 2. Let's copy and paste this one and let's also leave the BG neutral and let's add W full here. Let's give it a height of 78 pixels and That should be it let's just save this now and now let's go back inside of the index here and inside the dialog content let's create a proper grid which is going to render this description. So the reason I want to create a grid because our layout is going to look like this. On this side we're going to have the description and then we're gonna have the activity log down here but here in the corner we're gonna have actions to delete, discard or to copy it and on mobile mode they're gonna collapse underneath each other so for that I think it's best that we use the grid functionality.
So let's go outside of this conditional which renders the header and let's add that grid. So give it a class name of Grid. Grid calls one on mobile device so they go underneath each other and on medium device is going to be grid calls 4 and on medium devices they're also going to have a gap of 4 between another and now let's give this description right column a larger call span so I wanted to take three out of four possible places which we define here on the well medium mode right so call span three and then inside let's render a div which is going to use the full width of that column and now inside I'm just going to oh and let's also add space y6 because we're going to have multiple items inside but for now let's do the same thing with it above so if we don't have card data in that case let's go ahead and render description from .description which we just created. So just make sure you have all the necessary exports here so you can import it properly. So we're going to render description.skeleton.
Otherwise we're going to render the description and we're going to pass in the data, which is card data. And now I think we should already be seeing a nice little loading skeleton here. Let's try this again. There we go. You can see how for a second we have a nice little space for the description here.
So let's go ahead and style that now inside of this description component right here. So let's give this top div a class name of flex-items-center. Actually, let's make it items-start, gap-x-3 and w-full. And just as I wrote this, I think I remembered when I was watching the previous part of the tutorial that in my header component I made a typo. Yes, I saw this in the video so if you made the same typo, I don't think you did but just make sure this says items start, right?
So let's go back in the description now so flex item start gap x3 and full width and now let's add an icon align left from Lucid React so just make sure you have this import it is a self-closing tag and let's give it a class name of h5w5 margin top of 0.5 and text neutral 700. Below this icon let's open a div which takes in the full width and inside let's add a little paragraph which is going to be the label description and let's go ahead and give it a class name of font bold, actually semi bold might be better and text neutral 700 and margin bottom of 2. So let me just try that out to see how it looks. There we go, it looks quite nice. So this first skeleton is obviously representing that icon and the description text and now we're going to add that big box here at the bottom which is actually gonna be the description.
So this is gonna be conditional. It's gonna be a text area if we are editing it. Otherwise, it's gonna be a plain div. So for now, let's just make it a div and then later we're gonna make it conditional. So inside of this div let's give it a role of button so that it indicates that the user can click on it and let's give it a class name of min-height of 78 pixels, bg-neutral-200, text-small, font-medium, py3, px3.5 and a rounded medium.
And inside let's render the data description Or since it's possible that we don't have one, let's add a placeholder at a more detailed description. All right, and as you can see, this is how it looks now. So we have a nice little button that we can click here. And inside and right here, we're gonna have our actions, right? So let's go ahead and now, well, create the functionality to enable editing, right?
For that, we need to add all of those things like enable editing and refs so let's go ahead and prepare all of that here so first things first is editing and set is editing they're going to come from use state and by default they are false so make sure you import use state from react and while we are here let's also import useRef and elementRef now let's go ahead and let's create the text area ref and the form ref so const textAreaRef is useRef with the type of element ref, which uses the text area. And by default, let's give it null. And let's copy and paste this and now create a form ref, which uses the form element. All right. And now I just wanna add the query client and the params so const query client is use query client from 10 stack react query so make sure you have this import I will just order them by length.
All right and we're also gonna have the params. So const params is use params from next navigation. So let me just add this as well. Great so we have everything we need here and now we can go ahead and create our enable editing function. So const enableEditing() is an arrow function which set isEditing to true and setTimeout and then we're going to focus on the text area ref using the current focus.
Now let's go ahead and let's create the disable editing and that is very simply going to set editing false. Great and now let's create our own key down. So const on key down is gonna take in the event, which is the keyboard event. And if event key is escape, in that case, let's go ahead and disable the editing so a little bit of good user experience on our side and now let's go ahead and let's add the use event listener from use hooks TS and while we are here let's also import useOnClickOutside from useHooks so add both of those. UseEventListener is going to be listening on the keyDown event and it's going to call on key down.
And now let's add use on click outside of the form ref and that is going to trigger the disable editing. Great. And let's go ahead and create a const on submit here, which takes in the form data, which is a type of form data and inside of here let's go ahead and let's destructure the description so form data get description as string and let's also get the board ID to be params board ID as string. Great, and let's add a comment to do execute because we didn't add that yet. Perfect, and now we can go ahead and modify this div to render conditionally.
So let's wrap this inside of curly brackets and let's write if isEditing. Let's go ahead and let's just render a return of a paragraph. Let's just make it like this. Oh, actually this represents the return, my apologies. So inside let's run the paragraph which says editing.
And then let's wrap this in the else clause, the entire div which renders this like that. Actually like this, let's just see. So it should be just this div and close the else clause and you can remove this data the description here Like that so two divs here and one div here and we can indent this I believe yes, alright so now let's give this div an onclick enable editing great, and now let's go ahead and change this to not be a paragraph but instead be a form component and let's go ahead and give it a ref of form ref let's give it a class name of space y2. Now inside let's render the form text area component which we created recently so from s slash components form form text area and let's give it all the necessary props so we need the id which is a description we need a class name which is w full and margin top of two we need a placeholder which is add a more detailed description we need errors, well we're gonna have them later and let's give it a default value of data.description or undefined and outside of the form text area create a div which is going to hold our submit button and our cancel button So give it a class name of flex items center and gap x2 Let's render the form submit component Yes form submit component from add slash components form submit so make sure you add this import as well and inside let's just write save and now let's go ahead and render a regular button here which we can import from add slash components UI button like I did here and inside of here we're going to render cancel and now let's go ahead and give this all the necessary types so type is going to be button on click is going to be disable editing size is going to be small and variant is going to be ghost and now I think when I click in here there we go we have a nice little description to write in here perfect So now let's go ahead and actually create the execution for that and we don't have to create any new server action we can reuse our existing action called update card because in the schema you can see that we already prepared for accepting the optional description inside and in the index we just spread whatever values the user passes us so that way we cover both the title editing and the description editing so I'm gonna go here and I will import use action From hooks use action and I'm going to import update card From actions update card and now let's go ahead and I'm gonna go right here and I'm going to execute this I'm going to extract execute from use action I'm going to pass in the update card component and let's go ahead and let's get the onSuccess here and let's call the toast from Sonar so let's move the Sonar to the top here and let's go ahead and where is it?
It's right here. So we can also use the data here. So data.success, and we're just gonna open back ticks and we're going to write a card, open annotations, data title updated. So we don't need to render the entire new description because it can be pretty large. And inside of here, I also want to call the query client and I want to invalidate the queries, which is the card and data.id.
And let's add the onError which takes in the error and that is just going to well display that error in Atoast And let's also extract the field errors from here and then we can use that to pass in this form text area as errors. Great! Alright, so now I just think we need to add some refs here so let's actually we need to add the action here so action to this form is going to be the on submit right I think that's what we called it right here and now let's use the execute and let's pass in the description and the board ID. Let's see if I did this correctly. So we have the execute like that and something seems to not be working.
Oh, I think it's because of this. Board ID, it seems to be missing. It seems to be missing the title oh yeah or it's missing the ID yes ID which is data.id like that but I still think we have to modify the schema because in our action yes in schema we always require the title so let's make the title optional as well So go inside of your schema of update card and just modify the title can be optional like that and just wrap the entire thing in brackets and add a little comma at the end. Great! Like this and now we have no errors inside of here.
Great, and I think we did not assign this text area ref anywhere so let's also give that to the form text area here. Ref, text area ref. And let's try it out now. So I'm just going to refresh everything. I'm going to go in a random component here.
I will add a original description I'm gonna click Save and it seems like it is working Perfect and let's go ahead and refresh now to see if that's true. There we go. It says right here. The only thing I want to change is after I save, I want to disable the editing. So let's just go ahead and do that.
So I'm going to go right here and call disable editing. And I think that's going to be enough to trigger it back to the original. So I'm going to modify it again. I'm going to click save and there we go. It moves back to this.
Perfect. So we finished the description now. Now I want to create my actions and my actions skeleton. So let's go back to our index model. And outside of this div, which represents this column, we're gonna create a new one.
And inside, actually, we didn't have to create a div at all instead we're just going to render the actions here right so just leave a little space here and let's go inside the card model and create actions.tsx. Let's mark it as use client and let's export const actions here and the actions are gonna have its own div and don't forget to return this. Actions and what I want to do first is I want to create the skeleton for my actions so actions dot skeleton is equal to function actions skeleton and let's return a div with a class name space y2 and margin top of 2 and in here let's render the skeleton component which we can import from add components UI skeleton. Let's go ahead and give it a class name of width 20, height 4, BG neutral 200. Let's copy this two more times.
The second one is going to have a pool width and a height of 8 and the same is going to be for the one below. Now let's go back inside of the index right here and now we can conditionally render that. So if we don't have card data in that case, well, let's say it like this. If we don't have card data, we're going to render actions from .slash actions.skeleton. So make sure you import the actions in the same way with the description and the header.
So just make sure you have all the necessary exports in here. All right, and otherwise we're going to render actions, but we're going to pass in the data, which is card data. We don't have the types defined for that yet, but I think it should be enough. And you can see when I expand they go to the side here. So that's exactly what I want.
And when I refresh you can see how I have a nice skeleton for them on the side. Perfect. So now let's create the interface and let's wrap up the actions. So the interface I think you already know what it's going to be. Interface actions props data card with list from types.
All right and let's go ahead and assign those props actions props data and let's just make it properly spelled, there we go and now let's go ahead and style this UI so our first div is going to have a class name of space y2 and margin top of 2 and then inside we're going to have a paragraph which will say actions so let's just give it a class name of text extra small and font semi bold and then below that we're going to have two button elements so just make sure you import the button from here and the first one is going to say copy and it's going to have a copy icon from Lucid React above the text right so before the text make sure you import copy from Lucid React and while we are here let's also import the trash icon from Lucid React and let's go ahead and give this copy a class name of h4w4 and margin right of 2 and now let's give this button a variant of well we have to create a new variant because whichever variant we actually use for this model simply won't look good so before we do that let's go inside of our button component so inside of the components folder UI button, after transparent go ahead and create gray and let's give it a BG of neutral 200, text secondary foreground, hover BG neutral 300 like this.
So if you misspell it or something, or if it's not working, you can always visit my GitHub and just take a look at it here. And now let's give it a gray variant like that. And as you can see, we now don't have any errors And let's give it a class name of WFull and justify start but I also want to give it a new size And let's actually go ahead back inside of the button so we practice how to modify this size as well. So go back inside of your button after you've added this gray variant, go inside of the size here and create an inline size. And let's give it an HALTO, PX of 2, PY of 1.5 and text small.
Great and now let's go inside the actions and give it the size of inline. Great so that is our button now and we can copy and paste the button one below another and change this one to be delete and use the trash icon which we already imported. So let's try that out now when I click here there we go actions copy and delete. Perfect! So now we actually have to create the execution methods of those actions.
So we can actually kind of reuse both of those actions, right? Because they're not going to be very different from another. And let's just quickly visit... Okay, so I added text small to the inline okay everything is fine so we can close everything and let's go inside of our actions and let's for example copy well we can do the copy list why not And let's change it to copy card. Let's go inside of the schema and let's rename it to copy card like this.
And what's everything we need inside of this? So we need the ID and we need the board ID. I think that is fine enough. Now let's go inside of our types and let's change this to copy card. Like that and change this to be a board, sorry, card and returns a card.
And inside of the index here, let's also change the copy card from this schema all the way down here and change this to copy card as well. And now the authorization can stay the same. We are extracting, I believe, yeah, ID and board ID, that's all we need, but we're going to be working with a card, so just change this. And now let's remove everything inside of the try block. So it's completely empty.
And let's go ahead and first get the card to copy. So await db card find unique where ID matches and the list board has the same organization ID. If there is no card to copy, we can just return an error, card not found. Great. Now that we have our card let's find the last card in this list so const last card await DB card find first where list ID is card to copy list ID so we didn't need to manually pass the list ID because we can fetch it using this existing card we want to copy and let's order by order descending and all we need to select is the order itself.
And now let's write the new order. If we have the last card, it is last card dot order plus one otherwise it's just one and let's write card we await DB card create and let's use the data here let's give it a title to be open back ticks and we're gonna copy the original card to copy.title and we're going to give it a little tag copy at the end. Let's give it a description to be card to copy.description so that can stay exactly the same. Order, new order and list ID is going to be card to copy dot list ID. Great and now we are ready to return that card right here.
Perfect, and while we are here I think we can already go ahead and create our server action for deleting a card. So let's just copy and paste this and change it to delete card. Let's go inside of the schema here and change this to delete card as well. I believe we only need the board ID and the ID as we did in the previous one. So let's immediately go inside of our types and let me just change.
Oh yeah, I had a type on delete card inside of the schema and let's give this here, this here, perfect, go inside of the index, let's use the delete card schema, let's go all the way down, change it to delete card and the function rename to delete card as well. Perfect. So now I believe this will be even easier so we can just we can remove everything inside of the try block for the delete card because all we need to do is card is await DB card delete where id is matching and the list board has the matching organization id that's all we need perfect And let's change the error to be failed to delete and we pass in the card. Perfect. So we now have our two new actions and now we are ready to go back inside of our actions.
Do I have an error here? I believe it's just, let me just, I just pressed command shift B and reload my window to see if this will go away. There we go, it's no longer red, okay, no errors. So let's go inside of components, models, actions right here and let's import everything we need. So we need the use action from hooks use action we need the copy card from actions copy card and we also need the delete card from actions delete card like that perfect so let's go ahead and add those here so const use action copy card and let's write it execute to be execute copy card Let's copy and paste this and this one is going to be execute delete card and it's going to use the delete card action.
Let's go ahead and I believe we need some params here, so let's import useParams from next navigation and let's add the params here, so useParams And let's create const onCopy to be an arrow function which is going to get the boardId from params boardId as string and let's go ahead and call the executeCopyCard pass in the ID to be data.id and board ID. And we can copy and paste this function and rename it to onDelete. And this is gonna call executeDeleteCard, like that and now what I want to extract from here is also going to be well the is loading right so is loading for this one and is loading for this one as well and let's change it yes so is loading copy and this one is going to be is loading delete and let's now assign those actions to our buttons. So the first one is for a copy. So on click on this button is going to use on copy and disabled if is loading copy.
And now let's copy these two attributes and let's place them here. So this one is going to be on delete and is loading delete. Great. I just want to add some callbacks now and also when we delete a card I want to close the model right So let's go ahead and let's import our card model from useCardModel. So you can import that from hooks useCardModel and let's also import toast from sonar so we can display a success message.
So first let's do the copy card. So on success here I want to go ahead and log toast success and let's write card open annotations data.title copied. And I believe yeah we can close the model on copy as well so card model dot on close I think that would kind of make sense on error let's get the error and let's do toast.error. And now let me just copy these two actions here and just open an object here and paste them here. So this is going to be card data title deleted and it's also going to close everything and I think that should be it let's try it out so I'm just going to refresh just in case I will get this 1, 2, 3, first I'm going to try to copy there we go it's disabled and 1, 2, 3 copy.
Now let's try delete and it's deleted. Perfect so we can now delete our cards, we can copy our cards and we can also add the description. Let's actually try with description so I have this and if I click copy there we go the description copies as well. Perfect. So what we're going to do next is we're finally going to implement our activity tab here with audit logs and then we're going to implement Stripe.
Great, great job!