In this chapter, we're going to go ahead and implement our orders or purchases library. So in order to do that, we're going to have to create the library page UI and some library procedures. But before we dive into that, I want to explain the question from the last chapter about our product order relation. And I also want to address one maximum update depth error that we got within a use effect because of our use cart hook. As always, ensure that you are on your master branch and ensure that you have all the changes merged.
So now let's get to the question from the last part. So what happened is I made a purchase and that created an order. But when I went to my dashboard, it wasn't exactly clear whether it was connected or not. So if you go into the orders right now, you can see that now I have resolved it. So if you're asking how, very simple.
It's because I forgot that inside of the payload config, we have connected products to a tenant, which means that if I go with johndemo.com and look into my orders, because I bought this under John, I bought Antonio's product, I don't have access to look at their product inside of this payload CMS dashboard, but that's completely fine. I don't need it here. The only reason I even showed you this is just for you to realize that we now have this entity. These orders inside of this admin dashboard will be hidden for all users except for super admins. So if you are seeing untitled here in your order, just log out and then log in as the admin.
And once you log in as the admin here, you will see that the problem is no longer existent. So now what I'm gonna do is just gonna go back into my non-admin account here. So make sure you are logged in. And now I want to address the problem from the last time. So what happened is I added something from my cart here, right?
And once I purchased, I got this weird maximum update depth error. And I think I know why it happened. So inside of the checkout view, we have the use effect, which uses the clear cart here. So this is what I've noticed happens. Go ahead and comment this out and comment this out.
And then whatever your current URL of the checkout is, so for example, this, just pretend to add success equals true to it, and then copy this so you can test this out. So I'm going to open this now, And I will also add a console.log triggered here. So we know that this was triggered. There we go. So this was triggered three times, but let's see with success true.
You can see that it has been triggered 53 times and we can now consistently reproduce this error, which is a good thing. Whenever you can consistently reproduce something, it means it's easily fixable. The reason this is happening is because of clear cart. Clear cart is the culprit here. More specifically, the useCart method is the culprit.
So instead of getting the items like this, What I'm going to do is I'm going to properly use the selector here. So instead of this, I will do this. Basically, all of the methods from here, but using the proper useCardStore and then state getCardBy or whatever I need. So you can now remove this method. What this will do is it will properly extract that from the store and it will not re-render as much.
So if I change success to true now, I mean, if I just refresh here, just make sure that your URL is with success true, right? You can see that now there is no problem at all. This seems to be working exactly as it should, right? But you always have to be careful when you add functions to your use effect. So the reason ClearCart is now no longer problematic is because it was properly extracted through the ZustandSelector, right?
So these are selectors. So on a basic level, they ensure that no unnecessary re-renders happen when you use the selector. But we still have some functions here that we call, which should be memoized. The reason they should be memoized is if you ever plan on using them inside of a use effect, they would cause the same issue. So what I recommend doing is the following.
Find toggle product and turn it into a use callback. Like this. And add a dependency array. So import use callback from React. And inside of the dependency array, add add product and remove product.
And as for the product IDs, I don't like this solution, getCartByTenant. Let's go inside of useCartStore and let's remove getCartByTenant entirely. And let's remove that method from here. I don't like it. So let me just remove the get as well.
We don't need it at all. Instead, the way I'm going to do that is by using use cart store here. State, state, tenant carts, tenant slug, question mark product IDs. Like this. That's how I'm going to do it.
Or we can also add this. So it's always an array. And now I'm going to add product IDs here. And we also need the tenant slug. So now this method will only cause a re-render if any of these changes.
The remove product and add product are straight from selectors, so we don't have to worry about them. The tenant slug is a string, so that's completely fine. That's a simple type, right? But product IDs is actually a type of array. This is problematic because a type of array will never be the same.
So an array of one, two will never be the same as another array of one two, right? Because they're not a simple type. They're not a simple type. They cannot be compared like that. But there is a solution for this as well.
You can use use shallow from zustand. So go ahead and import use shallow from sustant react use shallow and add it to here. So just wrap in one more parentheses here. So now it's going to do a much better job of checking whether these arrays have actually changed. And I think that what it has got is just TypeScript server stopping working.
Yeah, you can see that now once I refreshed, it's completely OK. So I added use callback to the toggle product. I improved the re-render about the product IDs, and we improved the selectors of every single one of these. And now let's do the same thing here. So let's just add useCallback here.
And let's go ahead and add product IDs. And let's add useCallback here. What you can do now is you can have much more confidence in adding these functions and all of these things from here inside of useEffects. And we should also do the same thing for these ones, right? So add product here, let's just do const add product, handle add product.
And in here, we're just gonna do use callback. So you are basically just memoizing these. And do add product, tenant slug. And from here, we need the product ID, which is a string. Product ID.
Add product and tenant slug. So now I'm going to copy this and this will be handle remove product. Calling the remove product. There we go. So now all of these methods are properly memorized here.
So now I can add handle add product and I can add handle remove product here. Now I think that none of these should be problematic anymore. We should of course go throughout our app and check. So I highly recommend that you do this change because it should improve your app in sense of re-rendering and well, proper usage of the hook, right? So the selectors are definitely something we have to change.
It definitely makes sense to use shallow here. And depending on whether you want to use these methods inside of useEffect at any point, it will be crucial for you to have proper useCallback here. You might have noticed that for addProduct, removeProduct, clearCart, and clearAllCarts, we don't need useCallback. That's because it's automatically memoized from SushTand. That's why we just have to use the selector in a proper way.
So what I want to do now is just go through the app and see if everything works as expected. So I'm gonna go ahead and add to cart and looks like this has immediately updated to number one. If I click remove to cart, it's removed. Let's go ahead and click and see inside of the cart. I can load them.
Let's try removing from here. I can remove it from here. So I am not noticing any anomalies. I can add it. I can remove.
Let me do a refresh. It still works. I can go back here and add another one. I have two of them. Looks like we properly implemented this.
So now the question is, does this still work? And I'm pretty sure it does, right? So you don't even have to go through the whole checkout process. You can just pretend to add addSuccess true, and that should clear the cart. But this time, it shouldn't cause any maximum update depth errors.
There we go. No errors at all and the cart is most certainly cleaned. So I think we can say that we have officially resolved this error now. Great. So now that we have this resolved, this was also improved, right?
Because previously, this could have potentially caused a maximum update that exceeded. But now, clear cart is completely okay to be used. But also, make sure that you always extract them like this. Don't do, for example, cart. Because I think that this might, I'm not too confident to tell you, yes, this will cause problems, but I think it might if you do things like this.
I think that memoization kind of works in a different way when it comes to that. My apologies for not having the proper information to tell you right now, but something is telling me that that's not how you should be using it. I will try to research a bit to tell you with some more confidence, but what I am confident about is that this will definitely help us. We can now use these methods in useEffects safely. And we have definitely reduced the amount of re-renders now because useShallow is now actively comparing our arrays and seeing, okay, did the array actually change or did it not change?
Great. I'm very confident. If yours is behaving incorrectly for whatever reason, you can just revert these changes because It's not like our app wasn't working before. It just could have been improved and now we improved it. So what I want to do now is I want to create the library.
And one thing that I didn't write here, but I should have, is that we also need to change state on product view.tsx, right? So if I, for example, go inside of Antonio's product, And if I already purchased this product, it should tell me that I have purchased it, right? But in order to do that, we have to modify our procedure. So here's what I want you to do. I want you to confirm which orders you have.
So you can go inside of the dashboard here, Orders, and you can see that the user who has this order is John. So right now, I am logged in as John. This means that I have purchased something with this account. So just ensure that you are in an account that has done a purchase, something that has an order. And now we're going to go inside of procedures, inside of product procedures, specifically inside of get one.
Now this is by default a base procedure because we want to allow everyone to load a specific product. But what we're going to do is we're going to add headers from next headers. Let's remap them to get headers. And inside of here, inside of the get one, let's do const headers, await get headers, and then const session await context database auth, and just pass in the headers. So we are going to check in this public procedure if this user is maybe logged in.
And then after we load the product, before we return anything, I'm going to set isPurchased to false. And if session.user exists, meaning, hey, a user who is trying to visit this is logged in, I'm going to fetch an order. So const order await context database find collection orders. So this is orders data, actually. Pagination false limit one.
And in here, we're going to call where, and, and then two queries inside. The first one will be if product equals input ID, and the second one will be if user equals session user ID. So that's the unique combination we are looking for, a user that purchased a specific product. And then we're going to set isPurchased very simply to be true if order.ordersData.totalDocuments is larger than zero, meaning if it found an item. Or you can do ordersData.docs zero, and then just turn it into a Boolean like this, if it exists, right?
And then what you can just do is simply add that to this return. So isPurchased like this. And now you will have information if the user who just loaded a single product has purchased it or not. So let's go ahead and try and do that. So we are now going to go back instead of checkout view.
So in here we use not checkout view, product view. So not product list view, product view. In here, we use the get one. And that means now we have the isPurchased Boolean. So let's go ahead and modify the product view to use the isPurchased boolean now.
So I'm going to go and find the Purchase button. Let me just find the button. I must have skipped it. Oh, so it is right here, the cart button. OK, this is how we are going to do it.
If isPurchased, data isPurchased, we are going to display one thing, otherwise display the cart button. So If it has been purchased, we're going to display view in library, like this. And give this a variant of elevated as child prop, last name flex1, font medium, background pink 400, and inside give it a link. The link will very simply be prefetch and href which will go to slash library, and then data ID. So now, you can see that it says view in library after it loads, because it detects that I have purchased this.
So let me just see. Looks like this is causing hydration errors as well. So data is purchased, otherwise cart button. So maybe, maybe we can do this like this. Let's reuse our cart button and pass in isPurchased.
IsPurchased. Like this. Data isPurchased. And then instead of the cart button, go inside of cart button, add optional Boolean here, and let's use it here. And now, we're going to do that logic inside of the cart button instead.
So copy this. And what I'm going to do is very simple. If is purchased, That's what we're going to return. And you have to import link from next link. So this way, we don't have to handle the hydration errors in a complex way.
We already have it resolved because we import card button dynamically. And in here, data ID is product ID, exactly what we need. There we go. And perhaps I want this to be white view in the library. I feel like this makes it less similar than the other one, so it's clear that you have purchased this, right?
So you can see that now, if I go into a product that I haven't purchased, I can remove it or add it to the cart. But if I go to a product that I have purchased, I can only view it in my library, which right now will redirect me to something that doesn't exist. It redirected me and it thinks that it's a category. So we are now going to implement that category, sorry, that library. Instead of source here, modules, there is one more thing I just want to do.
When we check out inside of server procedures, it should be good to also confirm that we don't have any related orders, right? We should ensure that the person cannot purchase something twice. But I'm going to do that later, right? Now I want to focus on the library view. So let's go inside of source app.
And in here, we're going to create a library route group, and inside, library route with page.tsx div library. So now, if you go ahead and visit the library you should just see a text which says library Now let's go ahead and do the following. Let's return library view here. Now let's develop the library view. So we're going to add it inside of modules.
Let's create a new folder library and inside of here, UI, views, library-view.tsx is going to be fairly simple. So let's go ahead and add a div, oops, with a minimum height of screen and background of white. And as opposed to how we usually do it where we create layouts, we are not going to do that because it's not going to be a reusable layout in any way. This layout that we are building now is exclusively for this page and nowhere else. And when we actually visit the product from the library, it's gonna have a completely different layout.
So that's why we're not gonna reuse it. So now let's add a navbar here. So navbar with the class name of padding four, a background, full width, border bottom. And this navbar will simply have a link with an arrow left icon and continue shopping text. So Let's now import the library view so you can see what you're doing.
Of course, let's return this. And now you should see the text continue shopping like this. So when I go back, there we go. So you have to manually go to slash library to see it again. And now what we're going to do is we're going to create a header component below the navbar.
So let's add a header here with a class name, background, and let's just copy this color here like this, py8 and border bottom. Inside we're going to create a div which will have a strict breakpoint, so maximum width, and let's use dash dash breakpoint Excel, mx auto, px4, lg, px12, flex, flex column, and gap y of 4. Inside of this div, an h1 element, rendering the text library with a class name text of 40 pixels and font medium there we go now below that we are going to add a paragraph your purchases and reviews And give the paragraph a class name of font medium. There we go. Outside of the header here, we're going to create a section.
And let's give this a class name with the same breakpoint as above MX auto and the same padding except the PY and now inside we are going to create the actual product list. But before we create the product list, I want to create the actual procedure, which will allow us to load items in our library view. So let's go inside of our modules. And inside of the modules, you can copy the products server and paste it in the library. Let's go ahead and remove this import and let's rename the router to library router.
We can remove get one for now. Instead let's just focus on the get many which by default is going to be a protected procedure because only logged in users should be able to see things they have purchased, right? So we no longer need headers here. And we can just use the cursor and the limit for pagination, nothing else, which means that we can remove the complex querying here and immediately focus on this. But the collection will be orders.
And we are actually, for the first time, I think, going to use the depth 0. We want to just get IDs without populating. Let's remove the where and sort. There will be a where, but we are manually going to write it. So what we are looking for is all orders where the user is the currently logged in user.
So we are fetching all orders that that user created. And now we can extract the product IDs here from orders dot, my apologies, so this is called data, data docs map, get the order and simply return order dot product. In this case, product will be a string and not the product because we explicitly set the depth to zero. Let's call this orders data. Now that we have the product IDs here, we can get the products data here by going await context database find collection products pagination set to false, where ID in product IDs.
Now that we have the products data, we can spread the products data here and products data here. There we go. So that's going to be our method to load all orders and immediately load all. Yeah, now I'm thinking, you know, maybe we could just increase the depth and just use the product data from each order is what I'm thinking at the moment. But I'm going to leave it like this for now, right?
So it's this pagination part that worries me. But then I realized we cannot possibly have that many products inside of order. So it's actually fine. It's okay. So now that we have this, what we have to do is we have to go inside of the RPC routers app and we have to add the library router my apologies a library library router and of course import it now that we have that let's go back inside of our page for the library.
So in here, we render the library view. So what I want to do now is mark this as an asynchronous method. And in here, I want to get the query client. So get query client from the TRPC server. And then in here, I just want to do void query client, refetch infinite query, TRPC from TRPC trpcServer and in here let's do library.getMany.infiniteQueryOptions like this and in here I'm going to pass the limit to be default limit whoops default limit from constants and I think that's everything we need And then we're going to have to wrap the actual library view inside of hydration boundary from tanstack react query and we're gonna have to pass the state to be dehydrate from 10 stack React query, query client.
And I think this is the first time that I wrote this entire method without cheating by looking at an existing one. Great, so I think I finally got the hang of the new syntax of the RPC. Of course, I'm going to double check by searching for prefetch infinite query. Let's look at the one in the home page. So yeah, infinite query options, get query client, dehydrate.
I think that's it. Perfect. So now we are prefetching that for the library view. And now what we have to do is we have to implement the ProductList component. So what I'm going to do is I'm going to copy the ProductList from products here.
Go inside of UI components and just copy the product list component and paste it inside of library UI, not inside of views. Create a new folder components and inside just go ahead and paste product list. So inside of here, it's searching for product filters. We are not going to be using that, so we can remove this immediately. So we can remove this as well.
We will have product cards, so we can leave it. And the props will actually also be simpler. In fact, we're not going to have any props at all. Yeah, no props. Instead, we're just going to be calling trpc.library.
And the only thing we care about is the limit to be the default limit and the proper get next page. So this no products message can stay exactly the same as it previously was, but things will be just a little bit different here. For example, we are not going to need the narrow view at all. We can just fall back to a static class name. So no need to complicate it like this.
And in here, we can leave the same load more. So no more props in the product list skeleton either now. So we can remove this from the skeleton as well. And let's remove this and bring this up. There we go, as simple as that.
So now the only problem we have is the missing product card. This is something that we don't have yet. So what we can do is the same thing. Just go inside of Products UI, and let's copy the Product Card component. So click Copy, and go inside of Library UI Components, and paste it here.
Product Card, There we go. So we can leave the to-do here because we will have to do that here as well. And the card will more or less look exactly the same. So the first thing that's going to be different is There will be no router or user click here, so you can remove that, which also means that you can remove the imports here. The URL will also just lead to slash library and then the ID of the product, which means that we can remove this as well.
We are not going to have onClick here. So let me remove this onClick. I'm trying to see what will be different. And we are also not going to display the currency here. No need for that.
We already purchased it. And that's going to be all the changes. So if you want to, yeah, you can create a reusable component for this. But I don't know. I feel OK just having two components.
It's OK. It's not like it's reused 10 times or something. Now let's go inside of the product list here and we can now actually import the product card. The only thing that we're going to change is remove the price here. There we go.
So now, if you go ahead inside of your library, of course, we have to render this. So let's go inside of the library view. And inside of here, let's add the product list component. So make sure you import the one from the library, right? This one from your new library.
Don't accidentally import the one from the products. And we have to add suspense around this from React. And let's give it a fallback. Product list skeleton. Like this.
So from the same component. And I think that now, there we go. You should see a library which allows you to see your product here. Amazing. So now I want to give you access to the library button.
And you can already see it right here. So I'm not sure what exactly we coded, but if I go inside of my search input component, yeah, I put an href to slash library if we are logged in. So I guess that's fine. I guess we can do that. You can add refetch so it's faster.
There we go. So if you are logged in, you can access your library here. And in here you will basically see all the things that you have purchased. So I want to end the chapter here. In the next chapter, we're going to implement the actual view of the individual item.
And in that item, we will be able to leave a review. And then we can finally connect the ratings, right? And we can see the real ratings here. And we will be able to see the real ratings here. In the meantime, I will explore ways to protect us from purchasing a product we already own.
And yeah, I think that's going to be close to finishing. After that, we are going to implement, of course, the Stripe Connect and the ability to verify details and share the fee with our platform. But that part honestly isn't too difficult. We already have the majority of it set up. And then we have the deployment with infinite subdomains, which is also going to be super easy, right?
It's just going to be rewriting in question. You're doing an amazing, amazing job. So 21 library, let me see. We have done this. We changed the state on the product view.
We created library page UI and some library procedures let's go to 21 library now create the branch here so git checkout be 21 library just want to confirm there we go git add git commit, 21 library. And git push, new origin 21 library. Once you've confirmed that you have pushed this branch and you are on the new branch here, and you can see the detachment, let's go ahead and review our changes by creating a pull request. So in a second we're going to see what changes we've made. So the summary.
We launched a new library page that showcases user purchases and reviews. We introduced engaging product displays with interactive cards, lists, and animated loading states. So these are basically our skeletons, product cards, and product list components, which we have duplicated from the tenants module. We have enhanced the product view with purchase indicators and refine the cart interactions. These are basically the refactors we did and it's also mentioned here, the cart management mostly with performance improvements.
Exactly. Perfect, and we also optimized navigation through prefetching and integrated the new library functionality into the main routing structure. As always, we can see a more in-depth walkthrough here with file-by-file change. And here is an interesting sequence diagram for those of you who are interested in them. So this sequence diagram represents our logic to detect on a public get one procedure for fetching a product And in case we return user session, so if user session exists, in that case, we also query for the orders.
And if we return any matching orders, we set the purchased Boolean to true. Otherwise it remains false. And then we use the new is purchased prop to show the view in library button so a very nice representation of that functionality of ours so nitpick comments here we don't have to take care of this because we know that tenant will exist thanks to the depth property. In here it's considering adding error handling. We don't have to do that here since this is a suspense.
We can just add an error boundary the same way we add a suspense boundary. And in here I'm not going to change anything and I will not do this refactor so I am satisfied with this. Let's go ahead and merge the branch 21 library. There we go. Now I'm gonna go back to my previous branch, main or master, depending on what you use, and git pull origin, again, your branch name.
Git status to confirm everything is okay, and in the branch graph, confirm that as well. That's it. Amazing job, and see you in the next chapter!