In this chapter, we're going to go ahead and continue developing our products list by giving it a proper UI, as you can see here. In order to do that, we're going to have to create a reusable product list view component, we're going to have to create a product card component and loading skeletons as well as the infinite load for products. Let's go ahead and do that. As always ensure that you are on your main or master branch and that you have merged all the changes from before. After that, you can go ahead and do bun run dev.
What I want to do now is I want to go inside of modules, products, UI, and then inside of components. And in here, I'm going to create a product card component. Inside of here, I want you to have an interface like this, an ID name, an optional image URL, author username, author image URL, review rating, review count, and the price. So image URL and author image URL should have a question mark and or null type. So now you can go ahead and export a product card from here, along with extracting all the props that you have added right there.
So now I'm going to add a link from next link. So make sure you add this import. And for now the href will just go to an empty page. Below that, add a div with a class name of border, rounded medium, background white, overflow, hidden. And let's do height full, flex and flex column.
Now inside of here, go ahead and add a div with a relative and aspect square last name. And what we're going to render inside will be the actual image of our product. So give it an out of name, give it a fill property and a class name of object cover. And for the source, we're going to use the image URL. But image URL could potentially not exist, so we are going to fall back to an empty string.
Make sure you have added these two imports. Now let's go ahead inside of the product list component and instead of rendering this we're gonna go ahead and render the product card component. We can't really fill it with all the props we need, but we will do our best to make it look okay. So let's pass in the product ID as the key, the ID as product dot ID name as product dot name, image URL will be product dot image. And here's a cat, right.
So if you go inside of your products collection, you can see that we have added a image type upload with a relation to media. Right? So media is an object which has an alt and a source. But you can see that in here, it doesn't allow me to do not source. It has the URL property.
It doesn't allow me to do dot url the reason for that is the nature of get many here so if you go inside of the products router where we do get many where we do all the sorting and everything here We should modify our products here just a bit so they have the proper... So you can see that I wrote here, this will populate the category and it will populate the image. So we already covered the depth doesn't infer the types. So what we have to do is we have to modify the getMany method so that it properly assigns the type of image. So what I'm gonna do here is I'm going to spread data and add docs, data docs dot map, get the individual document here, spread the individual document, and then get the image.
And we're going to do document.image as media from payload types. So the same thing we did as type of category here, we are now doing as media here. But since this can potentially not exist, it can also be null. And now when you do the product list here, you can see that in here, you will be able to do ?.URL. Right now, you have no more of those errors, right?
But if you put the depth to zero, you will get most definitely an error because this will then be a string, right? So we have to take care and make sure that this is depth one. Great, So now we have the product card here, let's go ahead and add some more elements to it. So besides the image URL, we need the author username, which for now I'm going to hard code, for example, Antonio, it will be lowercase most likely, I'm going to leave out author image URL to be undefined, but I'm going to keep it here just so we know it exists. Review rating here will be 3, and review count will be 5.
So just some dummy numbers for now. And the price is something we have. So we can pass it here. Let's quickly go back inside of product card. And let's just fix this so we actually return this.
There we go. So now you should no longer have any errors inside of your product list. So let's go ahead and go to localhost 3000. So we can actually see this in place. And what should really happen here is only when you click on business and money, right?
So click on the parent category here and what you should see are two of your products, which look completely broken because there is no image added. So what you can do here is you can either go ahead and upload an image or you can use a placeholder.png. So where do you get this image? This can be whatever you want. For example, you can choose auth BG if you want to, or you can simply use the link in the description where I put all of my assets and just drag and drop placeholder.png like this.
Or if you have access to the source code, just go to the public folder and get it from there. Now let's go inside of product card and add placeholder.png. And if you refresh now, there we go. You can see how now it has a placeholder. Great.
So, the next thing I want to do is I want to go ahead outside of this div and add a new div with a class name padding 4 border y flex flex column gap 3 and flex 1. Then I want to add an h2 element which will render the product name and give it a class name of text large font medium and line clamp four there we go Now let's go ahead below this heading and add a div with a class name of flex items center and gap of two. And in here, I'm going to go ahead and do the following if we have our Thor image URL, only then go ahead and render that image. So I'm going to pass an out to be author username here. And source will simply be author image URL like this.
With will be 16. And same thing for the height. And we can put a class name here of rounded, old border, shrink zero, and size of 16 pixels. There we go. And then next to that, outside of the ternary, add a paragraph author username.
So we are only going to render the image if we have it. Give this a text small, underline and font medium. And I'm going to go ahead and give this an unclick here with an empty arrow function for now and add a to do redirect to a user shop. So now you should see the name of the user that created this, which in our case is just a hard-coded name because we don't associate products with any shops at the moment. So now what I want you to do is go outside of this div and check if review count is larger than zero.
So if this has some reviews, we're going to go ahead and give this a flex items center and the gap of one and a star icon. Give a star icon a class name size 3.5 and fill black. I have imported the star icon from Lucid React as always. And below the star icon, add a paragraph which will render the rating and then in parentheses, render the actual review count. Let's give this a text small and font medium.
There we go. You can now see the rating and how many people have rated, which is, again, something we don't have just yet, but we will have later. And we will also be able to do floats like this, but I think it actually parses it as a round number. So yeah, let's leave it like this for now. Now let's go outside of this div and add the last div here with a class name padding 4 and let's give it a class name relative px2 py1 border background pink 400 width of bit a paragraph which will render the price and a class name text small and font medium and there we go Might be a safer thing to do this instead.
Instead of just appending that, we can do new intl.numberFormat, n-US, style, currency, currency, US dollars, .format, and then parse as number the price there we go and if you want to I think you can also remove maximum fraction digits. I think you can put zero here and then it won't display the digits. If you want to, you can display the digits. So I'm going to put zero here. There we go.
Now let's go ahead and let's create the product card skeleton just below this, which is extremely simple. So export cons product card skeleton, a full width aspect three quarters, background neutral 200, rounded large, and animate pulse. That's it. Let's see what the errors are about. So the only error here is for the ID.
That's because we are not using it here. So for now, let's add an imaginary route, which does not exist yet, which will be slash products ID. And now you should have no errors at all inside of here. And this should look nice. Now let's add a quick hover effect to this so it matches our buttons.
So for that I'm gonna go ahead and use this hover effect. So let me add it to the beginning actually so you can see better. So hover, shadow, dash, 4 pixels, underscore, 4 pixels, underscore, 0 pixels, underscore, 0 pixels, and then underscore, and the black color in RGBA format. Make sure you put all of that in curly brackets. And inside of curly brackets, you must not have any spaces.
That's why we use the underscore. And also go ahead and add a transition shadow. And now when you hover, there we go. You have a similar effect as our buttons, but it doesn't actually move too much, right? I'm noticing some mistake here in our drop-down.
I'm going to address that later. But for now, let's go ahead and make this reusable so we can easily put that... Oh, looks like it's now fixed. I will explore how exactly this happens and why. Interesting.
Okay. Now what I want to do is enable this entire look on, for example, a subcategory. So in order to do that, we need to ensure that our product list alongside is actually reusable, right? So right now we have, I think, let me see, categories. I don't have a home.
Maybe do I have any views? I don't. So I think I have to go inside of app folder, app route group home, inside of category page. So this is where I have this view. So we are now going to modify this so it's a separate component.
So I'm going to copy this entire thing and I'm gonna go inside of my modules, inside of products, inside of UI, and I'm going to create a new folder called Views. And inside of here, I'm going to do product list view.tsx. I'm going to export const product list view here. And I'm just going to return this entire thing inside. Let's import the sort from components sort the filters.
Let's import suspense from react the production product list skeleton and product list. So we should have all of them here in a very closed folder because you just have to go inside of components, right? There we go. So we now have this and now we have to fix the props situation. So I'm gonna go ahead and create an interface props which accepts an optional category.
Let me just fix this into a proper bracket and this into a proper bracket. And then in here I will just assign that and extract the category. There we go. And now let's go ahead and let's use the product list view in the page instead of this. So I will remove this and just render the product list view.
And then we can remove all these individual imports because we now just need the product list view and we no longer need this even. There we go. So make sure you just added the hydration boundary here around the product list view And remember to pass in the category if it exists. And now, nothing should change if you are on the main category. So it should work exactly the same.
You should be able to sort in reverse. You should be able to sort something out, for example, like this, so there's no results. You should still be able to add individual tags here. There we go. But now we're just going to be able to do that in the subcategory as well.
So I'm going to go ahead and close this and let's go inside of subcategory so we see the situation happening here. And I think that we can just copy what we do in the category here. So I'm going to copy the page like this entirely. I'm going to go inside of subcategory and I will add the page here. And I will import product list view right here and I will remove all of these unneeded imports here and instead of category I'm going to have subcategory here because that's what we have here and we just need to assign the props here as well So let me copy search params from here.
We can import search params from NUX or NUX server. And we need to import load product filters from product search params. And instead of category, we need to do this. And in here we pass subcategory as well. And I think that's enough for this to work just as it does on the main category.
So if I click on... Yeah, it's a bit hard to use when you scroll. So if you're having problems with this, you can just use this one and then go inside of... Like this. There we go.
It works just as on our category page. Great, so we just finished creating a reusable component. We created a product card component. We created a loading skeleton. And now we need to do the infinite load.
So that's quite easy thanks to payload. Let's go inside of our products procedures in the server. So products server procedures And let's go ahead and just add a cursor, Z number, default one, and limit Z number, default, default limit from our constants like this. So the same thing we did with the tags, right? And then we're just gonna go ahead where we get the data and pass in the page to the input cursor and limit to be input limit.
As simple as this. Now we can go back inside of our product list component, not list view, so our older one, the list component, and in here we do useSuspenseQuery. So now we're gonna do useSuspenseInfiniteQuery. And I think I need to import that first from tanstack react query and instead of query options it's going to be infinite query options so that's going to be a set of two options here this is the first one so I'm going to pass the filters like this then the category and then I'm going to do limit default limit from constants. And then I'm going to do get next page param here, last page, and return last page docs length is larger than zero.
In that case, get the next page, otherwise undefined. There we go. That's all we need to implement pagination here. And now let's go ahead and just add, let's fix this and let's add the button to load more. So instead of data.docs, we're gonna do data.pages.flatmap, get the individual page, and then returnPage.docs.
And that should work exactly the same as it was before. And now, from here, you can also extract hasNextPage, for example and you can also extract is fetching next page and fetch next page so basically we need all of these Let me just prettify them a bit. There we go. And now what we're going to do is we're gonna go outside of this div. So let me see.
We can just do a fragment So we can add two elements inside. So just below this div, add a new div with a class name. Flex justify center, padding top of eight. If has next page, and only then. We're going to go ahead and render a button from components UI button so just ensure that you have added this as well.
So components UI button here and add load more. Give the button a disabled if query is fetching, my apologies, if is fetching next page. On click, we'll do fetch next page. Class name will be font medium disabled, opacity 50, text base, and background white, and variant will be elevated. So I think that now we might not see it at first, but if you change the default limit to one, it will then I think only load one element and you will be able to click load more until it loads both of them.
There we go. So just one more thing I want to do is an option to nicely display if products don't exist. So now we're going to do this. If data pages is completely empty, So data pages first in the array documents.length is zero. In that case, we're going to go ahead and return a div with a border, border black, border dashed, flex, item Center, Justify Center, Padding of 8, Flex Column, Gap Y4, Background Color of White, Full Width, and Rounded Large.
Inside, render an Inbox Icon and a paragraph No Products found. Give this a class name of TextBase and FontMedium. You can import InboxIcon from Lucid React. So now, even if you visit a category which doesn't have anything inside, you're going to have a nice display here. And let's just wrap it up by creating a nicer loading skeleton.
So that's gonna be pretty easy. We're just going to copy this div with the grid, add it here, and then in here, array from length default limit, like this, so we are consistent, right? That's how many items we expect to load. And then based on this imaginary new array, we're going to render product card skeleton with a key of index. That's it.
Product card skeleton from product card. And our product view, you can see already has suspense which uses the product list skeleton. So if we've done this correctly, you should see nice loading elements now. Excellent. That's it.
I think those are all the changes we needed we added the infinite load now let's go ahead and push this to github so I'm gonna go ahead and this is my 14 product list UI get checkout be 14 product list UI git add and git commit 14 product list UI git push you origin 14 product list UI Once you have confirmed that you are on the new branch and you have pushed that new branch to GitHub, let's go ahead and review our pull request and merge it. So I'm opening a new pull request here, and we're going to see what the next steps are. So, summary. New features. We launched a refreshed product view that combines detailed product cards, showing images, ratings, pricing, and more, with integrated sorting and filtering options.
We also introduced an infinite scrolling with load more button for seamless product discovery. It also noticed that some of these things were actually refactored. As always, we can see step by step for each change that we did here. And in here, we have two sequence diagrams. The first sequence diagram actually describes what we do with our prefetching, as you can see here with the load product filters.
And down here, it is describing our client-side infinite loading by using the Load More button, which requests new products based on the cursor and the limit. It correctly guessed possibly related PRs right here, and it also left some comments which are mostly related to the fact that we removed the default limit which again it was just for demonstration so I'm going to leave it at this so I'm gonna go ahead and merge this whole request I'm not going to delete my branch instead I'm just going to confirm that everything is in order there we go 14 product list UI and that is it we have pushed to GitHub and now as always Let's go back to main or master branch and make sure that you pull origin. There we go. Git status. You can easily confirm this by seeing the placeholder.
That means you successfully merged your new branch. Double check that you are on your master and confirm it here in the graph. Amazing, amazing job and see you