In this chapter, we're going to go ahead and build some basic product filtering here. So this is a screenshot of the component we are going to build. And I will primarily focus on the price filter, because tags require a whole another entity, right? So this is what I will focus on. But before we do that, in the previous chapter, we had an unresolved error with our prefetching, and that gave me an idea to improve TypeScript strictness inside of our app and resolve some TypeScript errors.
So as always ensure that you are on your main or master branch so that you have all the latest changes. Run git status to confirm and then do bon run dev. So this is what was happening. Instead of source app folder, inside of the app route group, you can see that we have a favicon.ico and inside of home you can see that we immediately have a dynamic category here. What does that mean?
So that means that localhost 3000 slash favicon.ico and the localhost 3000 slash business and money are treated as the same routes. And that is problematic for us because when we go inside of the procedures for the products, we don't check if the parent category exists before we give it a slug here. So that's what you're going to see happening now. So let me just try and go into a localhost 3000 here so I can demonstrate that issue happening in real time. So click on business and money perhaps, maybe refresh, and you will see that we have that error because it's going to be easier to look if you remove the console log here so I will just remove the console log there we go, you can see that we have a get slash favicon.ico, right?
You can see that they are on the same level. So our code gets confused and it thinks that this is a name of a category. And that's why we run into this error. So it treats favicon as a category and then it's attempting to read the slug from that category. There is an easy way to fix this which would simply be adding a new route in front of category.
So for example, you could create a folder called C and then just drag the category inside, right? And then all of your routes would have to be changed from localhost 3000 slash business and money to slash C business and money, which would resolve this issue, right? Because Fatican would never get treated as a category that way. So if you want to, you can do that. But then just make sure that inside of your modules, home, UI, search filters, you can find in folder and search for link.
And then you will have to modify all of these to have that prefix as well. So that's one way of fixing it. But here's a better way we can do if we really want to keep this structure. So there is a bug here that we were not alerted off. As you can see, the parent category here is automatically assumed to always exist, even if I did formatted data 10.
You can see that it never doubts that parentCategory.slug exists. But we know that we don't even have 10 products in our database. So how can TypeScript convince us that this exists? Here's a rule I want you to turn on immediately. I want you to go inside of your TSConfig and after strict, go ahead and add no unchecked indexed access set to true.
And once you save this right here, you will see that we now have an error because this is potentially undefined. An easy fix is to just move it inside of this if clause where we check if we have the parent category in the first place. So that's an easy fix for this. And I'm now going to remove this what I've wrote here. And I'm going to do some refreshes now.
And you will see that from now on, this will be the last time you will see this error right here. So let's go ahead and try again. Let's refresh. Let's click here. Let's click here.
Let's click in these ones, these ones, right? Just go ahead and click on as many of these as you want. And you will see that our favicon gets hit, but we don't match it as any kind of category. Now the problem is that it's still technically attempting to hit your category. So if you want to, you could do a check, if input.category, and then just skip that.
But you can leave it like this for now I'm gonna decide later if maybe we will refactor this to the do the example that I showed you with a prefix slash c right Now there's another thing I want you to do. I want you to go inside of modules. My apologies, inside of, yes, inside of modules, home, UI, components, search filters, and I think categories sidebar has a type error here. Categories get many output. It has an error here because of our, let me just find it, categories server procedures here because of this line right here.
You can go ahead and remove this line. I just wanted to ensure that our subcategories don't have subcategories, but looks like that actually messes up the types. So you can just remove that line from your categories router, get many. And now if you go back instead of the categories sidebar, you will see that we no longer have any type errors here. There we go.
And keep in mind, After you've changed your tsconfig.json to this, some more type errors might pop up now, because you can see that it's more stricter this way. All right, let's go ahead and let's develop our filters now. So what I want to do is I want to go inside of modules inside of products here inside of server and then inside of procedures and besides the category I am now going to add a minimum price which will be a type of string nullable and optional And then I'm going to multiply it, sorry, duplicate this and add max price here. So we're gonna have both minimum price and maximum price. And then in here, I'm going to check if we have input the minimum price, where dot price will have a greater than equal input minimum price.
And then we can duplicate this as well and check for maximum price. And in this case, it will be less than or equal maximum price. There we go. That's the backend part of our price filters. So what I want to do now is I want to go ahead and install a package which will help us with the front end side of things.
And just in case you're getting an error like this, I just went ahead and checked my error here. Client must be connected before running operations. So I immediately Google this, and it looks like it's Mongo client getting disconnected. So it's probably that what happened. There's nothing we can do on our side, I believe.
I can try refreshing, and you can see that now it works. So it looks like it was just some access to MongoDB, which got broken. Now let's go ahead and let's install Nooks into our project, which is a type safe search params state manager for React. And it's gonna make your life much much easier when it comes to params so I'm gonna do bun add nooks and I would suggest that you wait until you see what version I have so I'm gonna do bun add nooks and you can see it's 2.4.1 So what you should do if you want to use the exact same version is install this. Great.
After we have added NUX, let's go ahead and click on adapters and select Next.js app router. So we have to import this inside of our app layout let's go ahead and do that so I'm gonna go inside of source app folder app root layout right here I'm gonna go ahead and import this from Knox adapter And I'm simply gonna go ahead and I will wrap my entire app, including the TRPC provider inside, just like that. Now, what I'm going to do is I'm going to create a use product filters hook. So I'm going to go inside of source. And I'm going to go inside of products, modules here, products.
And I will create a hooks folder and inside use product filters dot es. And then I'm going to import useQueryStates from NUX. And I'm gonna go ahead and do the following. I will export const use product filters. And I will immediately return use query states here.
And inside of here, I'm going to define the minimum price and the maximum price filter. So minimum price will be parse as string. And I think you can import this from NUX like this and give it some options clear on default to be true. So this is just defining the behavior we want at the moment. And you can duplicate this and rename this one to maximum price.
So for now, These are all the filters that I want. And now let's create a component which is actually going to use this. So what I'm going to do is I will just focus on my category here. You can remove this. See if you didn't use it.
It was just for demonstration. I'm gonna focus on the category page. So for now I will not do anything in here. So category page. And what I'm going to do is besides having a product list I'm also gonna have a product filters.
So I'm just gonna go ahead and create a little scenery here, right? I basically just want to improve some things. There we go. So now just by refreshing, yeah, make sure you are in the main category. You can see that subcategory has no padding, but main category has padding.
That's because that's where we are working now. So make sure you are there. And now what I'm going to do further is I'm going to create a div, a grid. So div class name, grid, grid columns one on large grid columns six on extra large grid columns eight gap y six gap x 12. That's going to be the grid for the two elements which we're going to have here, one being a new product filters.
Let's give it a class name, border and padding of two, simply so you can see. There we go, product filters. So what I'm going to do now is I'm going to move product filters to be before the docs. There we go. So you should now see the product filters before you can see the docs.
And what we are going to do now is we're going to give this product filters another wrapper here, class name, LG, and it's going to be colspan2, Excel colspan2. Let's do that. And inside of here, around the suspense, Let's do a class name of LG column span four and Excel column span six, like this. So you should now have space for the product filters, and you should also have a nice space for the actual product list here. And now what I want to do is I want to enhance the products here just a little bit so you can at least see them as some cards.
So I'm going to go inside of the product list here. And In here, I'm going to give them a class name of Grid, GridColumns1, SM GridColumns2, MD GridColumns2, LG Large GridColumns2, Excel GridColumns3, 2Excel GridColumns4, and GAP4. Lg large grid columns to Excel grid columns three to Excel grid columns for and gap for. And let me just double check that all of these actually exist that I didn't misspell any this is a nice way to check that. There we go.
And then what I'm gonna do here is just product.myApologiesData?docs.map. And then we get the individual product here. And in here, we're going to give this a key of product dot ID like this and you know I just want to give it a little class name, order, rounded, and the background color of white, nothing too drastic for now. And let's just give it an h2 element, product dot name. Right, so just so we can start seeing some things appear here.
Product.name. And I think for now, this actually might be enough. And maybe let's add product.price here, under a paragraph here. And let's give this a class name of TextExcelFontMedium. So I think that now we should at least see these like that.
And let's give it some padding just so it looks like something. Great. And now it will be easier for us to see the actual filtering going on here. So let's go back where we started building our product filters and let's actually focus on product filters. So I'm gonna go ahead inside of my modules and I'm gonna go inside of products, inside of UI components and I'm going to build product filters.ts.
Let's export const product filters like this. And let's just build a nice little div here, and make sure to name this TSX. Let's give this a class name of Border, RoundedMD, and background color of white. Let's go ahead and create another div here. And let's give it a class name, adding 4, BorderBottom, Flex, ItemCenter, and JustifyBetween.
And now in here, we're going to add a paragraph filters and a class name font medium. Now let's go back to the page and let's replace this div with product filters. Just like that. There we go. So now you should see the text filters right here.
Excellent. So what I want to do now is beside it at a button clear with a class name of underline. So this is a native HTML button element and a type of button like this. And we have to mark this as a client component. So we don't get any errors here.
There we go. Perfect. So now what I want to do is I want to build a reusable component called product filter. So first I'm going to create the interface product filter props and then const product filter is going to use these props here. Whoops.
Like this. And we can just extract all of them and in here we're going to set is open and set is open so basically we are building like a little accordion here make sure to import use state from react then if is open we are going to use chevron down icon from Lucid React, otherwise chevron right icon. And I'm using the constant with a capital I so it can be used as a component. So let's go ahead and return a div here like this. And let's give it a class name of CN, which we can import from libutils, and passing some defaults, like this.
So padding 4, border, bottom, flex, flex column, and gap 2, and then passing class name if needed, which we have as a prop here, as an optional prop. Inside of here, another div with an onClick, which will simply toggle setIsOpen based on the current value and reverse the current value. And it will have a class name flexItemsCenter, justifyBetween, and cursorPointer, like this. And inside, let's add a paragraph with the text title. Now let's have a class name, fontMedium here, like this, and then let's render the icon.
The icon is this constant right here. And let's give the icon a class name of size 5. Outside of this div, If is open, we render the children conditionally. So now we have an easy way to render or hide some things, right? So what I'm going to do is I'm going to go back inside of my product filters, and outside of this div here, I'm going to add my product filter and give it a title of price.
And inside I will just add a paragraph price filter. And now you can see that we have a price filter here, which we can click And it opens just like that. And yes, we have a double border here, but don't worry about that. That's because this will not be the last filter that we are going to have. But if you want to, you can give it a class name, border B zero.
There we go. And you can see that now it looks better. Great. So we can now open and close this filter. And now let's go ahead and let's actually implement the price filter component.
So back in the modules for our products here, products UI components. Let's go ahead and create a price filter.tsx. Let's give it an interface, max price, minimum price, on minimum price change, and on maximum price change. And let's prepare a util format as currency. Basically we want whatever the user types inside to immediately be to immediately be formatted as a proper currency value.
So const numeric value is the first thing we're going to transform this on. So value replace. And inside of here, you need this regex right here. There we go. So we are basically targeting the numbers, something that's not a number, and we replace it with an empty string.
Like that. Now let's go ahead and ensure some decimal points. So numeric value split by dot, like this. Const formatted value will be parts, first part of the array plus parts dot length is larger than one. It means we have to add a dot.
Otherwise, let's attach to an existing one, question mark dot slice on the second position, or simply return an empty string. So this is a bit confusing. Basically, it's just ensuring that the decimal points look correct with this formatting method. Now let's go ahead and do this. If there is no formatted value, it means that this isn't a valid number.
So let's just return an empty string. Now let's do number value here and do parse float formatted value because we can properly parse it now because we checked with this. And let's do one more check. If is not a number, number value. In that case, we can return an empty string again, because something's not correct.
And finally, we can return new intl, which is built in API, and us style currency, currency, USD, minimum fraction digits is zero, maximum fraction digits is two, dot format, number value, which we confirmed is always going to be a number. So now what we need is a few things. Let's mark this as use client, and let's import change event from React and input and label from our components. And now we're going to be able to finally use this price filter with the minimum price, max price, on the minimum price change, and on maximum price change with the props here. And what I want to do is I want to return a div with a class name here, flex, flex, column, and gap of two.
And before we go any further, I want to go back inside of my product filters and I want to add my price filter here. You're going to have some errors even after you import it because it's expecting some types, some props which we don't yet have. But at least you will be able to see what it looks like. So keep it open so you can see what you're developing. Now let's add another div inside with a class name of flex flex column and gap of two.
Let's add a label here, minimum price, and let's give this label a class name of font medium and text base like this and below the label let's add the input component the input component should have a type of text and it should have a placeholder of $0 and then value will be minimum price. If we have it, form it as currency minimum price or just fall back this. And on change here for now, an empty arrow function. Let's go ahead and let's copy this entire div and let's change this to maximum price And the limit for maximum price will be, I will just use the infinity sign. You can find it on Google, just Google infinity sign, and you can copy it and paste it here.
And instead of min price, we use max price. There we go. So now you should see those two right here, but you are not able to change them at the moment. So now we're going to fix that. So let's go ahead and implement the handle minimum price change.
So handle minimum price change takes the event, which is a change event, and takes in HTML input element. And inside of here, we get the raw input value, and we extract only numeric values. So event.target.value, and then we go ahead and we replace all non-numeric values with nothing. So the same thing that we do right here, right? But we just do it before it even hits the on a minimum price change because on the minimum price change will be what's assigned to the filter.
So that's the handle minimum price change. And now we do the exact same thing, but for the handle maximum price change, right? Event target value that we replace, you can see this line is exactly identical, but we just call the on maximum price change right here. Great. So now let's go ahead and assign the values here.
The minimum price will be handle minimum price change. And this one will be handle maximum price change like this. HandleMinimumPriceChange and this one will be handleMaximumPriceChange, like this. I'm not sure why I'm getting this here. I think if I remove useClient, then it fixes.
So I just have to be careful. I have to use priceFilter inside of another parent component which has useClient, which in this case is not a problem. So now I'm still getting some errors because it cannot call the on minimum price change. So this is what we finally do now. If you remember, we added use product filters here.
So we can go ahead and finally use them now. So I'm going to go ahead and import them here inside of the product filters. My apologies, not product filter, product filters here. Const filters and set filters from use product filters. So make sure you import them like this, hooks useProductFilters.
And then what I'm going to do is I'm going to do const on change here, key type of, my apologies, key of type of filters and the value will be unknown. Whoops, like this. And set filters. So we are basically creating a reusable way to change any filter inside. Whoops, my apologies.
There we go. And now what we can do is the following. We can assign these two props to the price filter, the minimum price and the maximum price coming from our filters, minimum price and maximum price. And we can call our reusable on change for both of them quite easily. On a minimum price change, calls the value on change minimum price.
And on maximum price change, calls the value on change maximum price. There we go. So now, what should happen is that in your URL, as you can see, I currently have nothing in the URL. But if I type in 500 here, you can see that my URL changes to minimum price 500. But if I go ahead and type in 1, 000 in the maximum price, you can see that now it has both of those values.
And if I go ahead and remove one, you can see that now they are empty, for example. So this is what knocks those for us. This is what we are going to play around with. So what I'm going to do is I'm going to end the chapter here, and then we're going to continue with the filters in the next chapter. So this is what we aspire to do in this chapter.
We also had to resolve some TypeScript errors and we added some basic product filtering, but only as an introduction, right? So yeah, not exactly, right? We didn't add any actual filtering, but we did prepare the UI component at least. Now let's push to GitHub. So I'm satisfied with what we've done so far.
It's only UI, but it's fine. We also fixed an important bug. Let's do git checkout b12 filtering products. Let me just see. Filtering products, correct.
Git add, git commit, 12 filtering products. There we go. Git push u origin12 filtering products. And now let's go ahead inside of our GitHub and let's open a pull request here. Whoops.
And let's just create a pull request. And let's see what our reviewer has to say. So let's see what we've done in this chapter. We introduced an enhanced product filtering interface with interactive price filters for setting minimum and maximum values. We updated the category page layout to display filters alongside a responsive grid view of products.
That is exactly it. We can also see the step-by-step walkthrough of every single thing that we did here. We can see, of course, the sequence diagrams explaining how entering the minimum and maximum price notifies the price change, updates the query states, requests products with minimum and maximum price state, which is something that's not happening yet, but it is the next step. You can see how it already anticipates that we're going to do that, which is exactly what we're going to be doing, but we don't yet do this part. But still, very, very impressive for a zero configuration tool right here.
And it actually noticed a critical bug. So yes, these two actually override one another. Because if you have this, you would set it to this price, and then you would just override the where.price. So it offers a potential solution here. If it has where.price, then where.price less than equal is maximum price, but also keep it this.
But I think that there is an easier way to do this. So I'm not going to do any of these other ones, but I do want to fix our incorrect price filter. So what we can do is we can go inside of products, procedures, and I think an easy way to fix this would simply to be to spread this. Right? I feel like now there's no way that they won't be combined.
Right? We are of course going to test this in the next chapter, but I think this should fix it. So I'm just going to go ahead and while I'm in the same branch, I'm going to do a commit while filtering products fix. My apologies, git add and then commit and then just push the change. So now when I refresh here, we should have a new commit filtering products fix.
So I'm going to show you what our reviewer says about that and then we're just going to merge the request. I actually think that I accidentally introduced a new bug here because where.price does not exist initially. This will definitely break the code. I would rather we do exactly what CodeRabbit suggested here. Let's do that instead.
So I'm just going to check inside of the input maximum price if we have where.price. Let's do it like this. So I'm not going to spread this. If we have where.price, In that case, where price less equal, maybe I have to do it like this. Okay, this does not work.
Okay, this does not work. Okay, I'm not sure how exactly we should fix this because less than equal should be a part of this. But here's what we can do. We can do this for now. Const, actually, where.price can be equal an empty object here.
Or we can just put price here to be this. And then we will always be able to spread where.price. I'm going to go ahead and test this quickly if it just breaks the app or not. So it seems to be working correctly, like nothing is breaking, but I don't know the implications of price being an empty object. So here's what I'm going to do instead.
I'm going to check if we have input minimum price and input maximum price. So if both are passed, in that case, I'm just going to assign both values like this. Right? Greater than equal will have input minimum price and less than equal will have maximum price. And then I'm going to do else if input minimum price only, then I'm going to do where.price greater than or equal is input minimum price.
And I'm going to do another else if here, if we have input maximum price only, and we can just copy this and remove the spread. Yes, I over complicated this for no reason at all. Sometimes the brute force solutions are the best. I mean, the previously one obviously worked. But I don't know what happens if you know, price is just an empty object.
This seems to not break anything. It seems to be working. So I'm going to go ahead and add another third commit here. So add everything, git commit, 12 filtering products, fix 2, and git push. I'm not going to do any changes for this chapter anymore so let me go ahead and go back here.
I will just refresh and I'm just gonna wait for filtering products to finish review and then I'm gonna merge it. And there we go. So after this second fix, we can see another comment from CodeRabbit. I mean, I can see, right? And we have a confirmation that the implementation now properly handles all three cases.
Both filters, only minimum price and only maximum price. This correctly fixes the issue identified in the previous review where both filters would override each other. It goes further and it tells me that we should enforce with the parse float, but I think we have enough filters at the moment, so I'm going to go ahead and merge this. I am satisfied with these changes. I'm not going to delete the branch.
Instead, I will just confirm that we have it there we go 12 filtering products and now what I'm gonna do is get checkout back to my main or master and get all origin master or main depending on what you use get status for confirmation and a graph check. So I have one, two, three commits here. These last two are fixing the issue and then a merge back to the master branch. Ensure that you're on the master so you can continue with the next chapter. There we go.
And that's it. Pushed to the GitHub. Amazing job and see you