In this chapter, we're going to go ahead and implement cart and checkout. But we are only going to focus on the UI part. We are not going to connect Stripe in this chapter. I want to keep that as a separate chapter because there are a lot of things going on in that part. So for this chapter, what we are going to do is we are first going to implement the add to cart functionality using zustand or zustand global store.
And we are also going to implement the actual checkout page, which will in the future lead to a Stripe checkout page itself. Let's start with adding zustan, however we want to call this and add it to our project. As always ensure that you are on your master branch, ensure that you have all the changes merged. And let's go ahead and do bun add to stand and you can see that my version is 5.0.3 so if you want to you can use the exact version like this. After you have added that I think it's going to be enough for now and you can just run the project.
And now what I'm gonna do is I'm gonna go inside of source modules and I'm going to create the checkout module. And inside of here, I'm going to create a store folder And then use cart store.ts. And inside of here, I'm going to import create from zustand, and I'm going to add create JSON storage and persist. This is also from the zustand package, but the middleware extension. Let's create an interface tenant cart, product IDs string, and I mean an array of strings.
So what we're going to make this useCartStore do is hold each tenant cart. So we are not going to have just one cart. For each tenant, we're going to have their own cart. So now let's create an interface cart state. And first thing we're gonna have inside of the cart state is the tenant carts, which will be an object and then a key and then the actual tenant cart.
We're then gonna have the option to add a product using tenant string and the product ID we want to add. Also, an option to remove a product. We will have the clear cart and also we're going to have clear all carts. And we can also do get cart by tenant so just ensure that you have added the proper types here be mindful of the arrays after we've done that let's export const use create my apologies use card store and now we use create and we give it a type of card state inside go ahead and execute this open parentheses and in here add persist And now in here go ahead and add another set of parentheses, which is actually a method with the props set and get. And in here, go ahead and return an immediate object by wrapping it inside of parentheses like this.
Persists accepts two arguments. So here in the second argument, we're going to have to give it some properties about how we want to save this cart. So I'm going to give a name for this fun road cart and storage will be createJSONStorage localStorage. In case are wondering, where does the constant local storage come from? It comes from window.localStorage.
That's why there is no error. You can see that it's a global reference, right? In case you were confused, how come we never defined or imported local storage? You don't have to type window.localstorage. You can just use local storage.
So that's where we are going to store the cart data. And now let's go ahead and implement cart data starting with the tenant carts, which for starters can just be an empty array, empty object. And then let's do add product here which will accept the tenant slug and the product ID. And in here what we can do is we can do set. So be mindful that I didn't add curly brackets here.
I'm immediately returning set. I just added a new line so it's easier to look at. Go ahead and open set and then open another set of parentheses, which will not have set but state like this. And this will be a method which is going to return an immediate object again. And then tenant carts in here will preserve the old state of tenant carts.
And then for the current tenant slug that we are trying to edit, it's going to modify the product IDs. So this is adding a new product. So this is an array of IDs, which means that we first have to preserve all the existing ones. So let's do state.tenantCards, access the index using tenant slug. And this is where our esconfig no unchecked index access comes in handy.
Well, everything is in error now, so it's not really handy. But if you have this turned off, the TypeScript would assume that this always exists. But that doesn't have to be the case, right? So in our case, we have to add a ?.productids or an empty array. And then at the end, add the new product ID from here.
And just to get rid of these errors, I'm now going to copy this and add it here, and I'm going to call this remove product. And I'm not going to modify it in any way. Instead, I'm going to copy it once again and I will call this one clear cart. Let me just see did I do this correctly. Clear cart does not accept the product ID, I believe, just the tenant slug.
And I'm going to paste it one more time. Actually two more times. One for clear all carts, which doesn't accept any props at all. And the set here can actually be fixed to be very easy. Just tenant carts, empty array, empty object.
And last one is get cart by tenant we also accept the tenant slug here and then do get tenant carts access tenant by tenant slug question mark product IDs or an empty array. The reason I did this is so that we get rid of all those errors, right? So now you should have tenant carts, you should have add product, you should have remove product, clear cart, clear all carts, and get cart by tenant. We know that our add product is finished. Now let's do our remove product.
So it's gonna be quite similar. We have to preserve the state of the existing tenant carts, and then we just enter the slug that we are modifying. And then inside of here, we're going to do a little bit different logic for the product IDs we're going to do state tenant carts tenant slug question mark product ids dot filter inside of the filter we are going to get the ID of that product and check if that ID does not match the product ID that we pass in the function here. And then, outside of that filter, we are going to add or an empty array. So we are basically filtering out the product ID from specific tenant slug.
Now let's go ahead and do our clear cart here which is going to be much simpler. So what's important here is that we preserve all other tenants, but when we enter our tenant here, we are simply going to change product IDs to be an empty array. Now for the clear all cards, it's self-explanatory. We just return clear all cards to its initial state here. There we go.
That is the functionality which we are going to do for carts. Obviously there's room for improvement, right? How many products can we actually have in our carts now? How do we invalidate this? How many tenants can we actually serve?
And maybe a burning question some of you might have, why different carts for different tenants? Why not just one big cart for everyone? That was my plan initially. I wanted to do just one cart, which is going to haul products from any tenant, regardless of the slug. Unfortunately, when it came to implementing Stripe Connect, where we are going to add it so that our platform takes a 10% fee from each sale, it didn't work if the author of the product that we are selling is not the same for all products.
The only way it worked was if I changed my implementation. And that new implementation required me to add real business details. And no matter how much I try to find an alternative, try to find maybe I'm somehow exiting the test mode or something. Looks like they didn't implement a way to do that without real bank information, without real company information, which is something that I'm sure a lot of you will get blocked by in this tutorial. I'm not sure if you have real company information at hand here.
So that's why I decided, okay, I'm going to use the simpler method. And I'm sure whoever completes this tutorial will definitely be handy enough to change this later on and experiment with one global store for all tenants in the future. But for now, this is the path I'm going to choose because I know it works. It's definitely not bad. It's a practice that you will see in many multi-tenant marketplaces.
And we will get our achieved result. Great. So ensure that you have your use cart store. And now what I want to do is I want to create the hooks folder here called use cart. So in here, I'm just going to make it easier to use this store.
So you can immediately import use cart store and then export const use cart. And then slug string. And then, my apologies for the typo here, const. We're just going to destructure a bunch of items from useCartStore here. GetCart by tenant, add product, remove product, clear cart, and clear all carts.
And now let's go ahead and get all product IDs by that tenant. After that, let's create a method to toggle product, meaning add it and remove it with one same function. So product ID, which checks if in the list of these product IDs for that tenant we have that product, then remove it, otherwise add it. It's important that you preserve the tenant slug across these places. Let's add another handy method, isProductInCart, which will simply tell us if the product IDs for this tenant include a specific product ID.
And lastly, let's do clearTenantCart, which will simply clear cart for a tenant slug. And now what we have to do is we have to return product IDs, add product, which will do product ID, string, add product, tenant slug, product ID. So we are basically using this method, but we are automatically assigning the tenant slug. So it's just easier to use. We do the same thing for remove product.
Basically preserve the tenant slug here. Clear cart will be clear tenant cart. And then we just pass along clear all carts, toggle product, these product in cart, and total items, which will be product IDs dot length. There we go. So now we have a much easier way of using the cart store.
Now I want to go ahead back inside of the product view, not list view, product view. Basically this page, right? You can also do a bun run dev to ensure that it's loading here. And what I'm going to do is I'm going to enable the Add to Cart button to do that. The way I'm going to do that is by creating a component here, which I'm going to call cartButton.esx, like this.
Let's go ahead and create an interface props which will accept the tenant slug, which is a string, and export const cartButton. Inside of here, let's grab the props, let's assign them here, and let's extract the tenant slug. Then what we are going to be able to do is get our cart functionality using useCart, not useCartStore, just by using useCart and passing the tenant slug. This will enable us to easily add, remove, or toggle items based on that slug here. And what we're going to do inside is just copy, let me find my button.
Here it is, button add to cart. So I'm just going to copy it here and add it in here from components UI button. And then we can do things like if cart is product in cart, we should also pass in the product ID here. Let me just add it product ID. So is product in cart product ID.
In that case, we can do remove from cart, otherwise add to cart. Like that. So let's go ahead and try it. And we also have to do on click here. Cart, add product, product ID.
So we don't have to pass the slug, because inside of the use cart, add product automatically appends the slug here. So now let's replace this with the cart button. And let's pass in the product ID to be data.id and tenant slug to be, I'm not sure if I receive it as tenant slug, I do. So I can pass tenant slug here. So I actually have both.
I have both product ID and tenant slug. So I don't have to use data ID. I can just pass the product ID. I already have it as a prop. Why not leverage it twice?
So now, your button should look exactly the same. But let's go and open our application here. Specifically, let's open the local storage for localhost 3000. I'm going to clear everything I have inside. Keep in mind that you might have some things here.
Localhost 3000 might have some values from your other developments. Let's click Add to cart. And as you can see, it has immediately changed to remove from cart. And I have fun road cart state here. Let's go ahead and take a peek at the state.
Inside of the state, I have tenant carts. And then for Antonio, I have a list of product IDs. So now it came to my mind that instead of add product, this should be toggle product. So just use toggle product. And now click remove from cart.
And you can see that now my tenant cards for Antonio has been removed. And now I can add it to cart again. And now what I want to do is import CN from lib utils and just modify this a little bit. So if cart is product in cart product ID in that case I want to add background white here just so we have a difference. There we go.
Great. So why did I add this to a separate component? Well, besides the fact that it works easier this way to maintain it, This can be a part of hydration errors. As you can see in here, I think this is exactly what happened. Hydration failed.
Let me zoom in. Hydration failed because the server rendered HTML didn't match the client. Basically, on the server, we don't have access to local storage. And I don't think there's an elegant way to add it there. I think if you want it there, you have to add it to server storage, like a database.
I will explore if there are some smarter options. But I'm OK with using sustain here. I'm OK with this only being client-side. So basically, client-side knows that it's in CART. So it's going to render the text remove from CART, and it's going to render background white.
Server does not know that. So server renders remove from cart, my apologies, add to cart, and it renders background pink. And that's a hydration error. The server and the client don't match. But there's an easy fix for that.
Instead of importing cart button like this, we're gonna go ahead and import dynamic from next dynamic. And then we're going to change the way we import the cart button. Let's go ahead and do const cart button. You can comment out this import for now. It's dynamic.
And go ahead and import. And you can use normal import method. I think you can just do dash dash components cartButton, like this. But cartButton itself is a named export. So we have to tell it which one.
So let's do dot then. You get the actual module. Don't call it module, Call it mod. Module is a reserved keyword. So mod.cartButton.
There we go. And then SSR false. So now it's not going to go through server-side rendering. And now we don't have to worry about the mismatch between the server and the client in this part. So now you will be able to see that when I do this, it's going to disappear for some time.
So there is no chance of the hydration error happening now. If you go ahead and comment this out, let's comment this out, and let's enable this back, and make sure it's in CART. And if you refresh, you will pretty much always see the hydration error, right? But if you change it to dynamic and turn off SSR, you will no longer have that issue. You will also no longer have this at all.
So what you can do is you can add a fallback here if you want to. You can do loading prop here. For example, a paragraph of loading. And I think that now you should see the text loading while it loads, right? But we can do better.
We can add the button component like this. And let's keep it as add to cart. So we're gonna assume it's not added into cart and we can add the variant. Well, actually it doesn't need a variant. All it needs is flex one.
So let's give it the class name of Flex1, and let's give it a background pink of 400. And let's just make it disabled. So we are basically doing a skeleton. I wouldn't recommend exporting a skeleton here, because I'm not sure what happens if you add this import. You're going to have to add an import from cart button.
So I'm not sure what exactly happens in that case. Perhaps you will trigger the hydration error. So I think that now, there we go. It looks much better, right? It's almost like it's loading the state and you can't click it and it's not even elevated at that point.
Great. I like this solution a lot. We can now add and remove from cart and we are also not getting any hydration errors. And you can do the same fix for this page here. I think that when you are logged in, we render.
You have to be logged in, I think we re-rendered the library button. But I think many times I've gotten a hydration error there. Right now, I'm not getting the hydration error. But if I get it again, I'm just going to do the same thing for the library button. I just want to remind you of that.
And I think this looks completely okay. Now that we have this add and remove to card functionality, I want to display the card here in this corner of each tenant store. So we're going to do a similar thing here. I'm gonna go inside of checkout. Let me just go here.
So checkout and inside of here I'm going to create the UI and I'm going to create components. And in here we're going to have the checkout button.tsx. So the previous button we have created in the products here because it's only used in the product cart, in the product view, right, as an add to cart. But this one is more of a, you know, global let's go to the checkout screen button. So that's why I'm going to call it checkout button.
Let's go ahead and create an interface. Checkout button props, optional class name, and optional hide if empty. Actually, I'm going to hide if empty by default. Or... I don't know.
Let's keep it like this for now. Now we can go ahead and export the checkout button using class name hideIfEmpty and checkout slug. My apologies, tenant slug. Now let's go ahead and let's grab our use cart. It's in the same module, so we can use an import like this.
Now that we have the use card, let's extract the total items for a specific tenant. And now we're going to do a check here. If hide if empty is on and total items is zero, you can return null. Now let's go ahead and grab a component and some utils. So import button from components UI button CN and our generate tenant URL.
Make sure you use the capital URL here. Generate tenant URL here. Great. So, we are now going to go ahead and do a return here with a button, variant, elevated, as child, last name, CN, background white, but pass the class name if needed. And then a link from nextLink.
That's why we need it asChild. And add an href here generate tenant URL tenant slug slash checkout and then let's add a shopping cart icon from lucid react and then beside let's check if we have total items. If total items is larger than zero, in that case, render the total items. Otherwise, just an empty string, so we won't take up any space at all. Make sure you import the icon.
Now that we have our checkout button, we can go back inside of the tenant module, UI, navbar. And after the link, which redirects the user's shop, we can render the checkout button and pass in the tenant slug, which is the slug. We have it from the props here. So I think we're going to have the same problem now. You can see that I have one item in here, and when I refresh, at first I don't, which causes a hydration error.
So we're going to go ahead and do what we just did in the product view. I'm going to import dynamic from next dynamic. Let me add that here. And then I'm going to modify the way I import my checkout button. So let me copy how I do it for the cart button here.
And I will add it here, call it a checkout button. I'm going to go ahead and comment this out. But I will just copy this. And we are targeting the checkout button from that module. And then we have to create a loading state here as well.
So let me import a button here. What's important here? Let's go inside of the checkout button from our modules. So background white is important. That's it.
And we're going to add the shopping cart icon inside. So let's add the shopping cart icon and background white and I don't think it uses any flex one here. Just make it disabled. So now your checkout button should be disabled at first. Looks like it cannot load the shopping card icon.
I'm not sure. Maybe it can't do that in the loading states? I'm not exactly sure. Maybe it's something else. Maybe the shopping cart icon has an invalid color.
Yes, Looks like it works that way. What if I remove text black? Then it doesn't work. Looks like when you are adding loading, you need to specify text black, which is something we don't need to do Otherwise, as you can see right here. Let's remove from cart and there we go.
Now we have an empty checkout button up there. So we can now decide do we want to hide that when it's empty or not. And as you can see now, you should not be getting any hydration errors at all. So if you want to, you can go inside of your navbar here. You can now remove this import for the checkout button.
If you want to, you can add hide if empty. And then when you don't have it, it's just not going to appear. So whatever you prefer. Maybe you like it this way. Maybe I like it this way as well.
And now let's go ahead and let's just add a skeleton here. So I'm going to go ahead and copy this button. And I'm just going to add it here. That's going to be the skeleton for when the navbar is loading because the navbar has to load the information. So when I click on Antonio here, you can see how I now have a skeleton with that button.
Looks like it's also showing me... Oh, okay. So I have this. Let's do this. So I removed Antonio's product, and now I'm going to go inside of John's shop.
So you can see in John's shop, I have one here, right? But if I go to Antonio, I don't have it because my cart for Antonio's shop is completely empty. So initially I wanted to have the cart here, but as I explained when we built the useCart functionality, turns out we can't do it with proper Stripe Connect and fee sharing method. So I chose the next best thing. Great!
So what we have to do next is we have to implement the checkout screen, which I'm actually going to do in the next chapter, because this one is already half an hour, and I want you to see what you've done and play around with it a little bit. But we've also done a lot. So we have implemented the cart functionality with Sustan global store. We modified the UI in product view, and we separated the cart button component. We also added dynamic load without SSR to avoid hydration errors.
The only thing we didn't do is implement the checkout page, which basically means that we didn't use this picture in the end. But it's okay, we're going to do it next time. So now let's go ahead and push our changes to GitHub. 18 cart and checkout. So I'm going to go ahead and do git checkout dash b18 part checkout git add git commit 18 part and checkout and git push uorigin 18 part and checkout After you confirm that you are on the new branch, that you have detached, let's go ahead and let's merge this pull request.
So I'm going to go ahead and open a new pull request with base master or main and comparing to my new branch here. And let's see what our next steps are. And here we have the summary. We introduced interactive cart button on product pages, allowing users to add or remove items. We enhanced the checkout experience with newly integrated button in the navigation that displays real-time cart item counts.
You can see that this pull request introduces several enhancements here using a custom hook use cart on top of the store which we use to manage tenant-specific carts. As always, you can see file by file change and in here a sequence diagram, if you're interested, explaining how our cart button system works. You can see that when the user clicks toggle product button, we call the toggle product from the use cart hook which contacts the cart store and it checks whether the product is in cart or not and depending on that it calls the respective methods and then we return the updated cart state and update the UI. We also do the same thing for the checkout button. We also have the renders nothing, hide if empty prop.
And I'm proud to say no comments besides some nitpick comments here such as an extra comma at the end. So we did a pretty good job on this pull request. Let's go ahead and merge it. After we merge it, I'm not going to delete it. I'm just going to ensure that I have it right here.
There we go. And then once it's merged, I'm going back to my main or master branch and I'm just going to go ahead and pull origin and ensure that everything is up to date. Hit status, there we go. And I can see that I detached here, and then I merged those same changes back to my master branch. Excellent.
That's it for this chapter. And then in the next one, we're going to focus on the checkout page UI. So let's mark this as completed push to GitHub. Amazing, amazing job and see you