So now that we have all the functionality finished in our board, I want to go ahead and create audit logs, which are basically logs of activity, which will be, well, created every time we do some kind of change, like renaming a card, copying a card which is essentially creating a card, right? Deleting a card, editing the description, all of those things will have its own activity and then you're going to be able to see that. So just like in Jira or original Trello, you're going to be able to see exactly what happened with a specific card. And of course, we're also going to add that activity right here in this tab, which says activity, which is currently a 404 page. And before we do that, I just quickly want to expand this model right here because I have a feeling it's just a bit too small.
So let's see how we can do that. I want to go inside of components UI dialog right here. And we already modified this BG black here but let's take a look right here in the dialog content and in here you can find the max width lg. How about we change that to 3xl instead and now I believe that our model there we go is slightly bigger so we have more room for this activity right here. Perfect.
So where I want to go now what I want to do now is first let's shut down the app because we're gonna be modifying not ports terminal because we're gonna be modifying our schema so shut down your app and let's go ahead inside of prisma schema dot prisma right here and all the way at the bottom let's add a new model. Model audit log. But in order to use that first we're gonna have to create a couple of enums. So first let's create an enum action. So we're gonna have the possibility of logging create actions, delete actions, actually let's write update first, so update actions and delete actions and we're also going to have an entity type which is going to be connected to each of those actions which are board, list and card.
Great and now let's go back inside of the audit log here and let's give it an ID of string ID and a default value of UUID. Now let's give it an organization ID, which is a string. Let's give it an action, which is a type of action, which we just defined above. Entity ID, which is just going to be a string entity type, which is going to be a type of entity type enum, which we defined above the same as action. We're also going to have the user ID, which created that.
We're going to have the user image, string db text, and user name, string db text as well. And we're going to have the created at so we can order them by latest with the default value of now. And we're going to have the updated at which is also a date time with the value at updated at and I'm just gonna go ahead and align all of these things I just like them to be aligned you don't have to do this of course but you know I think Prettier does this for you but I'm not using Prettier in my tutorials because I don't want you to miss out on any changes in the code great so now that we have this we have to push it in the database. So let's go ahead inside of the terminal here and let's run npx prisma db push. So this is going to add this new model inside of our database and now we also have to do npx prisma generate so we locally have this.
And again make sure you've shut down your app so now run it again because if you kept it running well there's a chance that it wouldn't be registered and then you're gonna have some weird errors until you restart your app and now I want to go inside of the lib here and I want to create a new file createAuditLog.ts so we're gonna have a reusable function whenever we want to create a log And let's go ahead and import out and current user from at clerk next to JS. And let's import action and entity type from at Prisma client. And let me just change this to be out like that. Let's import database from, well, I'm gonna change it to lib like this. And let's create an interface props to accept the entity id which is a string entity type which is entity type entity title which is a string and action which is an action and now let's go ahead and let's create this function so export const audit create audit log is going to be an asynchronous action which calls the props and now let's open a try and catch block so in case this fails we don't want to break the entire function right we just want it to live in its own scope so console.log let's write audit.logError and let's log that error here.
And now in here first let's extract the organization ID from auth and now let's extract the entire user from await current user so both of this are imported from Clark Next.js and let's go ahead and check if we don't have the user or if we don't have organization ID in that case throw new error user not found. And now let's go ahead and let's extract entity ID entity type entity title and action from props. And let's do await DB audit log, which we now have create and give it a data of organization ID, entity ID, entity type, entity title. Did I misspell that? Entity title.
Looks like something is wrong here. Did I forgot to add it to my schema let's go inside of Prisma schema here yes I forgot so add entity title here which is also a type of string like that and now what we have to do is shut down our app since we modified the schema, run npx prisma generate again so that's gonna fix it here as you can see now we no longer have that entity title error and then we also have to do npx prisma db push to synchronize it with the actual database on planet scale or wherever you're running your database and then npm run dev and everything should be fine. Great, besides entity title We also need to pass the action, we need to pass the user ID which is user.id, we need to pass in the user image which is going to be user?imageURL and we're also going to need the user name which is going to be user first name plus empty space user last name. Like that. So that's it.
That is our audit log action. So now let's go ahead and add it to a very simple action like for example creating a card. So in order to prepare to see whether this is working or not I'm gonna go ahead and also open my Prisma Studio. So npx Prisma Studio, make sure you have this running and let's just paste it in here. So I have no audit logs you can see it says zero here so I'm gonna click here and focus on that.
So there are no rows in this table And now let's go ahead and let's close this and let's go into a very simple action create card and let's go inside of index right here and let's go all the way to the bottom here where we assign the await DB card create to the card constant and below that we're gonna add await createAuditLog which you can import from s slash lib createAuditLog as I did right here I'm just gonna move it here and let's go ahead and give it everything we need so create audit log entity id is going to be card id we're going to have entity title which is going to be card title we're going to have entity type which is going to be entity type which you can import from app prisma slash client so entity type dot card and we're going to have an action which is going to be action which you can again import from prisma client dot create like that So let me just show you my imports. So I also added this action and entity type from Prisma Client and create safe action from s-lib create safe action.
Perfect! So let's try out if this is working. I'm gonna refresh this. In my Prisma Studio I have no audit logs right now so let's just wait for this to... Okay and let's create a new card test let's click create it's creating and there we go the card has been created let's check my Prisma studio here I will refresh the audit log and there we go I have the organization ID the action is create I have the entity ID I have the entity type card and the entity title matches my card and I also have the user ID user image user name everything I need perfect So now we are ready to create an API route so we can fetch that activity inside of a card model and display it here.
So let's go back inside of our API folder. I'm going to close everything here, go inside of app, API, inside of card ID and create a new folder, logs. And inside create a new route.ts. Let's export asynchronous function get here. Let's get the request, which is a type of request.
Let's extract the params, which are a type of params, which have the card ID, which is a string. Just confirm, of course, that this card ID matches the card ID folder. Let's open try and catch block. Let's log the error here and let's just return new next response internal error with a status of 500. Now let's, oh we have to import next response from next slash server so make sure you do that and in here we're going to go ahead and extract the user ID and the organization ID from the out library from clerk nextjs and while we are here let's also import the database from s slash lib db and now let's go ahead and check if we don't have the user ID or if we don't have the organization ID return new next response unauthorized with a status of 401.
And now let's get const audit logs we await db audit log find many and let's go ahead and write where the organization id matches entity id is params card id and entity type is entity type which you can import from Prisma client entity type dot card like that and let's order by created at descending and for the card model we're just going to take three so only the latest three are gonna be visible there but in the activity page you're gonna be able to see all of them. Great. Now let's go back inside of the card model so components models card model inside of the index here and in here we already fetch the card data so we can copy and paste this function and replace this to be audit logs data and let's change this query key to be card-logs let's change the fetcher to go to slash API slash cards slash id and then slash logs and change the expected output to be audit log from Prisma client and add a little array at the end. So I imported audit log from here. Great.
And now that we have this audit logs, it's time for us to render them. So inside of here, let's create a new component called activity.dsx. Let's mark it as useClient and let's export const activity and let's return a div saying activity here. Now what I want to do is create a little skeleton for it so let's write activity.skeleton to be function activity skeleton and let's return a div inside which is going to have a class name of flex items start gap x3 and w full it's gonna have a skeleton which you can import from s slash components UI skeleton and go ahead and give this skeleton a class name of BG neutral 200 and let's also go ahead and give it a height of 6 and a width of 6. Now let's go ahead and create a div with a class name of WFull and inside we're going to have two more skeletons like this.
Let's go ahead and give this one a width of 24 and a height of 6 and let's give the bottom one a width of full and height of 10. Like that. And let's give this upper one a margin bottom of 2 as well. Perfect. So let me just save that and now let's go back inside of our index here and here we rendered the card data.
So let's copy and paste this below and let's change this to use the audit logs data and it's going to render the activity from activity and activity here and we're going to pass in the items here to be audit log data like that. So let's just go ahead and quickly create an interface for the activity here. So interface activity props is going to have items which are a type of audit log from Prisma client. Perfect and I think we should not be having any error here let's see we seem to be having errors oh we didn't assign that yes so audit activity props and extract the items. Great So now when you click on a card, let me just refresh, when you click on a card, there we go, you have this lower loading state right here and it seems something is happening with our request.
Let's check that out so right here audit logs data API cards ID logs and let's check my API route so app API card ID logs export asynchronous function get oh I forgot to return the audit logs so let's go back inside of the route for logs here and after we fetch the logs let's return next response JSON audit logs like that. Alright and now let's refresh this and let's pick another one and there we go. Now you can see how it stops loading after a certain time. Now let's go back inside of our activity component inside of our card model here and let's go ahead and give it some styles so class name is going to be flex items start gap x3 and width of full we're going to render the activity icon from Lucid React and make sure you import it as activity icon or if you can't do that because they have the activity icon but you can see it clashes with the name of our component so you can always remap it using as activity icon or you can just simply import the activity icon but you know just in case they change it in the future or something and now let's give this activity icon a class name of flex items start gap x3 sorry no I copied the one from above.
So height of 5, width of 5, margin top of 0.5, text neutral 700. Let's create a div with a class name of widthFull. Let's create a paragraph which says activity and let's go ahead and give this paragraph a class name of font semi-bold text neutral 700 and margin bottom of 2 and then let's create an ordered list here with a class name of margin top 2 and space y 4 and in here let's iterate over our items like this and let's just create a paragraph item and this should be item of course so item dot let's just say entity title for now just so we see something here let's try it out so not all of our cards are gonna have activity just the recent one which we created and there we go you can see we have some existing activity here because it knows that this is the name of our component sorry of our card and now let's create a reusable activity item component before we do that I just quickly want to create a lib which is going to be called generate log message so we don't have to write our message ourselves every time.
So inside of the lib folder create a new file generate log message.ts and let's go ahead and import action from Prisma Client and audit log from Prisma Client as a type and now export const generate log message is going to be a log, audit log, let's go ahead and extract the action entity title and entity type from log and let's do a switch on the action in case it is action.create we're going to return backticks and this should be without column here we're going to return created entity title sorry entity type to lowercase entity type to lowercase and then we're going to open annotations and render the entity title. So if it is the create action, we're going to say created a card and then the name of the card. Like that. And let's go ahead and just copy these actions. So let me just indent them properly.
Like that. So the second action is going to be update and we're here we're going to say that we updated and the last one is going to be delete and in here we're gonna say deleted and then the last one is going to be the default case and we're just going to say that it's an unknown action because we don't know exactly what happened if for any reason we don't have that. Great, so now that we have the generate log message let's go inside of our components and let's create a new file activity item dot tsx so let me just close this so alongside our hint and our logo here Let's go ahead and let's export const activity item like this. Let's go ahead and create a type for it. So interface activity item props is going to have the data audit log from Prisma client and now let's also import the generate log message from lib generate log message and let's also go inside of our terminal here and let's run npx chatsien-ui at latest add avatar because we're going to need to display the image of the person that created this specific log right so okay that's fine and let's also import that so import avatar from .slash uiavatar and avatar image and I'm just gonna replace that to use components and now let's assign the props so activity item props let's extract the data from here and let's go inside and let's return a list element and let's give it a class name of flex-items-center and gap-x2 and let's write an avatar here which we have imported let's give it a class name of h-8 and w-8 and let's render the avatar image So we have both of those imported from here, right?
And let's give this one a source of data user image. And now below that let's create a div with a class name of flex flex-col and space-y 0.5 and inside open up a paragraph and let's give this paragraph a class name of text small and text not test text small and text muted foreground and inside let's open up a span and render data user name and give this span a class name of font semi-bold lowercase and text neutral 700 and then just outside of this span here add a space and run generate log message and paste in the data like that and then just outside of here we're gonna add a new paragraph which is going to well say when this was created and for that we're going to need to install npm I datefns. So just make sure you install datefns so we can work with dates now. So let's go to the top here and let's import format from date FNS and now here we're gonna go ahead and render format new date data dot created at and we're gonna format it in a type of MMMD, YYY Y and then open this small annotations at and then we place the time HMM8 like that and give this paragraph a class name of text extra small text muted foreground like that perfect now let's go back inside of our activity inside of the card model here and instead let's render the activity item from components activity items.
So make sure you import that. And let's go ahead. Let's give it a key of item.id and let's give it data of item. Like that. And let's try that out now when I click on test.
There we go. It says that I created the test. And now what we have to do well for every new card that we create that's going to be true so it's going to be having the new audit log here. And now we have to add audit logs for all the other actions inside of our projects so just go inside of the actions and we're going to go inside of each of those and add where we can. So let's start with the copy card go inside of the index here and in here where we assigned a new card go ahead and await db sorry await create audit log from add slash lib create audit log and give it an entity title to be card.title go ahead and give it entity ID to be card.id, entity type to be entity type from prisma.client, so make sure you import that, .card and we're gonna have action to be action again from prisma.client.create like that.
So let me show you my imports. I imported the create audit log here and I imported PrismaClient action and entity type. And we're gonna do that for all of those. So I'm actually just going to copy this so I can easily do it. So you can copy createAuditLog as well.
After copyCard let's go in the side of copyList. And in here where we assign our list, Let's go here and let's import create audit log from lib create audit log. Let's use the list title. Let's use the list ID and import the entity type from Prisma Client and the action from the card. Great.
And let me just align my imports here. Great. Alright and now let's go into the next one. So that's going to be our create board. So go inside of the index here.
After we create the board, go ahead and create a new audit log use the board title and board ID import the entity type and import action. Great! Now let's go inside and let's change the entity type of course yes I think I forgot that in the list as well. So change the entity type here to be board right and go inside of the list and change the entity type to be list as well. So that's important.
Alright and the copy card one is correct. Now let's go inside of create list index right here and we do the same thing so find the list right here and let's import create audit log. We are using the list and let's import the entity type, let's import the action so the action is correct but the entity type needs to be list. All right now let's go inside of delete board, go inside of index here and after we delete it go ahead and import create audit log from lib slash create audit log. We are using this board and entity type is going to be board and our action so we make sure you import both entity type and the action is going to be delete.
Now let's go and fix the delete card so all the way to the bottom here let's add create audit log let's use the card the entity type is card make sure you import the entity type and the action delete is correct because we delete here as well. Now let's go into the delete list and we're doing the same thing, so find where we deleted the list in the try block let's import create audit log let's use the list information we import the entity type, we import the action, the entity type is list, but the action is correct. Now let's go inside of the update board index here and let's paste that here as well so after we update the board import create audit log, use the new board the entity type is board and the action is update great, and make sure you save those files of course. Go inside of update card now and it's going to be pretty similar here so create audit log, we're going to use the card, import the entity type which is a type of card and import the action which is still update and go inside of we're going to skip the update card order and update list order and instead we're going to go just for the update list here so when we change the title of the list Let's go ahead and create audit log here and let's use the list information and let's use the entity type from Prisma Client and let's use the action and that is the same action.
Perfect! So now whatever we do we have of course our audit logs. Again just make sure that you add the necessary imports from Prisma Client and create audit log. Great! But what happens so right now you can see when I try let's try it out So when I edit something here it should give me a new...
Oh yes we forgot we need to revalidate this as well. So I want to go back inside of the card model so go inside of components, models, card model here and we have the header right. Let's go inside of the header first and in here we do some query client invalidate queries. So let's just copy this and let's also re-invalidate the card log if that is the correct one. So it's card logs.
So re-invalidate the card logs and now let me just refresh, pick a random one, change the title and now it should re-invalidate with the new logs and they're right here and they will also work, it will not work for the description so we have to do the same thing in the description so copy the invalidate queries go inside of the description here and let's see where is our own success it's right here so after we re-invalidate the card also re-invalidated the card logs. So let me try that out now. When I add a new description there we go it says that we updated the card. And if you're wondering about this fact that sometimes it's empty well this should never happen right? It's because we created these cards before we implemented audit logs.
So after you reset your database, right? When you're going into production or whatever, you're gonna see, always see some activity here or you can use Prisma Migrate Reset. Perfect, so we finished the activity here and let's just try for copy for example when I click copy here it also has the created card exactly what we want and now we finally are able to create the activity tab here which is going to be quite easy because we have a lot of reusable components here. So let's go let me just expand this Let's go inside of the app folder, platform, dashboard, organization, organization ID and in here create a new folder called activity. Like that.
And inside of activity create a new file, page.vsx. And let's go ahead and write const activityPage let's return a div activityPage and don't forget you always have to export default when it comes to pages and layouts. So activity page like that and let's try and click on activity now and there we go no longer we have a 404 error. If you're still seeing a 404 error confirm that you have named the activity correctly and then you have to confirm inside of your nav item here in the routes that you're in this routes constant that your activity also doesn't have a typo here. So just some possible culprits that could happen.
Great and now inside of this page let's go ahead and let's give this a class name with full. Let's go ahead and let's enter the info component from dot dot slash components info so we have it right here the info component like that. Let's add the separator from components UI separator. Let's give it a class name of my2 and then let's go ahead and let's render the activity list which we don't yet have but we're gonna create in a second so go ahead inside of activity here create a new folder underscore components and inside create activity dash list dot dsx like that It's going to be a server component. So no need to put use client on the top.
Activity list is going to be an asynchronous function. And let's extract the organization ID from out from clark-next.js if we don't have the organization ID in that case we can redirect the user using Next Navigation to select org. So make sure you import redirect from Next Navigation. And while we are here, let's also import the database. Let's import the activity item from components activity item and let's import the skeleton from components UI skeleton.
Now let's go ahead and let's fetch all the activity logs for this organization. So audit logs are await DB audit log find many and let's go ahead and write where organization ID. That's all we need. And then let's go ahead and let's return an ordered list here and give it a class name of space Y4 and margin top of 4. Let's add a paragraph, no activity found inside this organization.
And this is only going to be visible if there are no items. So we can do that using CSS, using the hidden by default, but only If it is the last element inside of this ordered list, it's going to be visible using the block. And text extra small, text center and text muted foreground. And inside of here, let's do audit logs dot map. Let's get the individual log in here and let's render the activity item let's give it a key of log ID and data of log itself great and now let's just go ahead and create the skeleton for this one so activityList.Skeleton is activityListSkeleton function activityListSkeleton And let's go ahead and let's return an ordered list and let's give it a skeleton here and let's write class name width of 80% and the height of 14 and let's give this ordered list a class name of space y4 and margin a top of 4 and now let's just copy it a couple of times so let's give this one a 50% width, let's give this one a 70% width, this one an 80% and last one a 75% like that, so make sure you have this skeleton now go back to the page.vsx right here and let's go ahead and do the following let's import suspense from react let's wrap the activity list inside of suspense here.
Let's import the activity list from .slash components activity list so let me just separate my imports just a bit and now let's go ahead and give it a fallback of activity list.skeleton. Like that! There we go! Now we have the activity page. So you can see in here we shouldn't have any activity and now let's try out and create a new board for example so I'm going to create a random board here and I'm gonna go back to the activity here and there we go.
It says that I created a board. Perfect! So let's try and renaming the board now and let's go ahead and let's add a list here and besides the list let's also go ahead and create a card all of those things let's copy a card for example and let's delete a card so all of those things Let's go back now to the activity and there we go. Created the card, created the board, created the card, updated the board, created a list. We have everything we need in here.
And just one more thing that I want to do and this thing I think yeah it should be like this activity list dot skeleton or not oh I have a mistake here skeleton yes and I think yeah we need to render it like this great and now just go inside of the activity list and also add order by created at descending state is in proper order otherwise it doesn't give us too much information there so we created the board we updated the board created the list created a card created a copy card and then deleted a card. Perfect! And you can see how we have a loading state for our activity. Great, great job! All that's left is to create the payments.