Skip to content

Commit

Permalink
Feature/add product filters (#141)
Browse files Browse the repository at this point in the history
* feat: add reusable filter component with categories and price range filters arrays and neccesary imports

* feat: add reusable filter component with categories and price range filters

* feat: add apply filters and reset functionality

* feat: integrate filter component with product page

* style: make filter UI responsive and accessible

* fix: installed all dependencies

* style: make filter UI responsive and accessible

* fix: Removed unused input import

* fix: Removed comment

* fix: Deleted react icons and replaced react-icon-filter with lucide-react-filter

* fix: fixed alignment on desktop

* refactor: use button instead of div for a11y compliance

---------

Co-authored-by: Derian <[email protected]>
  • Loading branch information
Nomolos29 and derianrddev authored Feb 11, 2025
1 parent 2a552ab commit 57f87ee
Show file tree
Hide file tree
Showing 11 changed files with 1,469 additions and 176 deletions.
4 changes: 2 additions & 2 deletions apps/frontend/app/marketplace/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ export default function MarketplaceLayout({
return (
<div className="flex flex-col min-h-screen">
<Header />
<div className="container mx-auto py-4">
<div className="container mx-5 md:mx-4 py-4">
<BreadcrumbNavigation />
</div>
<main className="flex-grow container pb-10 mx-auto">{children}</main>
<main className="container pb-10">{children}</main>
<Footer />
</div>
);
Expand Down
62 changes: 38 additions & 24 deletions apps/frontend/app/marketplace/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Image from "next/image";
import Link from "next/link";
import { useState } from "react";

import Filters from "@/components/marketplace/filters";
import ProductFilter from "@/components/marketplace/ProductFilter";
import ProductsNotFound from "@/components/marketplace/products-not-found";
import { ProductsPagination } from "@/components/marketplace/products-pagination";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -41,43 +41,57 @@ const getProductKey = (id: number) => {

export default function ProductList() {
const { t } = useTranslations();
const [priceRange, setPriceRange] = useState<[number, number]>([0, 1500]);
const [priceRange, setPriceRange] = useState<string[]>([]);
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);

const filteredProducts = products.filter(
(product) =>
product.price >= priceRange[0] &&
product.price <= priceRange[1] &&
(selectedCategories.length === 0 ||
selectedCategories.includes(product.category)),
);
const handleApplyFilters = (filters: {
categories: string[];
priceRanges: string[];
}) => {
setSelectedCategories(filters.categories);
setPriceRange(filters.priceRanges);
};

const filteredProducts = products.filter((product) => {
// Filter by categories
const matchesCategory =
selectedCategories.length === 0 ||
selectedCategories.includes(product.category);

// Filter by price ranges
const matchesPriceRange =
priceRange.length === 0 ||
priceRange.some((range) => {
const [min, max] = range
.split(" - ")
.map((s) => Number.parseInt(s.replace("$", "")));
return product.price >= min && (max ? product.price <= max : true);
});

return matchesCategory && matchesPriceRange;
});

return (
<>
<div className="container mx-auto px-4 py-6">
<div className="flex flex-col md:flex-row gap-6">
{/* Filters */}
{/* ProductFilter */}
<aside className="w-full md:w-1/4">
<Filters
priceRange={priceRange}
setPriceRange={setPriceRange}
selectedCategories={selectedCategories}
setSelectedCategories={setSelectedCategories}
/>
<ProductFilter onApplyFilters={handleApplyFilters} />
</aside>

{/* Product List */}
<section className="flex flex-col flex-1">
<section className="flex-1 overflow-hidden">
{filteredProducts.length <= 0 ? (
<ProductsNotFound
setPriceRange={setPriceRange}
setSelectedCategories={setSelectedCategories}
setPriceRange={() => setPriceRange([])}
setSelectedCategories={() => setSelectedCategories([])}
/>
) : (
<div className="grid flex-grow grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredProducts.map((product) => (
<Card
key={product.id}
className="hover:shadow-lg mx-auto sm:mx-0 max-w-[24rem] sm:w-auto"
className="hover:shadow-lg w-full max-w-[24rem] mx-auto sm:mx-0"
>
<CardHeader>
<div className="aspect-square">
Expand All @@ -96,7 +110,7 @@ export default function ProductList() {
width={320}
height={320}
priority
className="w-full h-full rounded-t-lg cursor-pointer"
className="w-full h-full rounded-t-lg cursor-pointer object-cover"
/>
</Link>
</div>
Expand Down Expand Up @@ -138,6 +152,6 @@ export default function ProductList() {
<ProductsPagination />
</section>
</div>
</>
</div>
);
}
3 changes: 2 additions & 1 deletion apps/frontend/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
},
"iconLibrary": "lucide"
}
268 changes: 268 additions & 0 deletions apps/frontend/components/marketplace/ProductFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
"use client";

import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Check, ChevronsUpDown, Filter } from "lucide-react";
import { useState } from "react";

interface FilterCriteria {
categories: string[];
priceRanges: string[];
}

interface ProductFilterProps {
onApplyFilters: (filters: FilterCriteria) => void;
}

const productCategories = [
"Electronics",
"Furniture",
"Appliances",
"Sports",
"Fashion",
"Books",
"Toys",
"Art",
];

const productPriceRanges = [
"$0 - $50",
"$50 - $100",
"$100 - $200",
"$200 - $500",
"$500+",
];

const ProductFilter = ({ onApplyFilters }: ProductFilterProps) => {
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedPriceRanges, setSelectedPriceRanges] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [opened, setOpened] = useState(false);

const handleCategoryToggle = (category: string) => {
setSelectedCategories((prev) =>
prev.includes(category)
? prev.filter((c) => c !== category)
: [...prev, category],
);
};

const handlePriceRangeToggle = (priceRange: string) => {
setSelectedPriceRanges((prev) =>
prev.includes(priceRange)
? prev.filter((p) => p !== priceRange)
: [...prev, priceRange],
);
};

const handleApplyFilters = () => {
onApplyFilters({
categories: selectedCategories,
priceRanges: selectedPriceRanges,
});
setOpened(false);
};

const handleResetFilters = () => {
setSelectedCategories([]);
setSelectedPriceRanges([]);
onApplyFilters({ categories: [], priceRanges: [] });
setOpened(false);
};

const filteredCategories = productCategories.filter((category) =>
category.toLowerCase().includes(searchQuery.toLowerCase()),
);

const buttonStyle = "w-full md:w-[48%] text-lg h-10";

return (
<main className="max-h-screen md:h-auto">
<div className="space-y-6 w-[300px] hidden md:flex md:flex-col">
{/* Categories Filter */}
<div className="w-full">
<h3 className="font-semibold mb-2">Categories</h3>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-between">
{selectedCategories.length > 0
? `${selectedCategories.length} selected`
: "Select categories..."}
<ChevronsUpDown className="h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput
placeholder="Search categories..."
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandGroup className="h-[250px] overflow-y-visible">
{filteredCategories.map((category) => (
<CommandItem
key={category}
onSelect={() => handleCategoryToggle(category)}
className="flex w-full justify-between"
>
{category}

<Check
className={`ml-2 h-4 w-4 ${
selectedCategories.includes(category)
? "opacity-100"
: "opacity-0"
}`}
/>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>

{/* Price Range Filter */}
<div>
<h3 className="font-semibold mb-2">Price Range</h3>
<div className="space-y-2">
{productPriceRanges.map((priceRange) => (
<div key={priceRange} className="flex items-center space-x-2">
<Checkbox
id={priceRange}
checked={selectedPriceRanges.includes(priceRange)}
onCheckedChange={() => handlePriceRangeToggle(priceRange)}
/>
<label htmlFor={priceRange}>{priceRange}</label>
</div>
))}
</div>
</div>

{/* Buttons */}
<div className="flex space-x-2 w-full justify-between">
<Button
variant="outline"
onClick={handleResetFilters}
className={`${buttonStyle}`}
>
Reset
</Button>
<Button onClick={handleApplyFilters} className={`${buttonStyle}`}>
Apply Filters
</Button>
</div>
</div>

<button
type="button"
className="fixed right-10 bg-[#ffffff9d] hover:bg-[#fff] shadow-md justify-between rounded-full text-black pr-3 p-[6px] pl-4 flex gap-x-3 top-20 font-medium items-center text-lg md:hidden"
onClick={() => setOpened(!opened)}
>
Select filter
<span className="p-1 rounded-full bg-black text-white flex justify-center items-center">
<Filter />
</span>
</button>

{opened && (
<div className="gap-y-6 w-[97%] h-[60%] py-10 p-5 rounded-t-3xl flex flex-col justify-between fixed bottom-0 left-0 bg-[#e7e7e7b6] backdrop-blur-xl text-black z-50 md:hidden">
{/* Categories Filter */}
<div className="w-full">
<h3 className="font-semibold mb-2 text-xl pb-2">Categories</h3>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full justify-between bg-[#99999960]"
>
{selectedCategories.length > 0
? `${selectedCategories.length} selected`
: "Select categories..."}
<ChevronsUpDown className="h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="max-w-[400px] min-w-[330px] p-0">
<Command>
<CommandInput
placeholder="Search categories..."
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandGroup className="h-[250px] overflow-y-visible">
{filteredCategories.map((category) => (
<CommandItem
key={category}
onSelect={() => handleCategoryToggle(category)}
className="flex w-full justify-between"
>
{category}

<Check
className={`ml-2 h-4 w-4 ${
selectedCategories.includes(category)
? "opacity-100"
: "opacity-0"
}`}
/>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>

{/* Price Range Filter */}
<div>
<h3 className="font-semibold mb-2">Price Range</h3>
<div className="space-y-2">
{productPriceRanges.map((priceRange) => (
<div key={priceRange} className="flex items-center space-x-2">
<Checkbox
id={priceRange}
checked={selectedPriceRanges.includes(priceRange)}
onCheckedChange={() => handlePriceRangeToggle(priceRange)}
/>
<label htmlFor={priceRange} className="text-lg">
{priceRange}
</label>
</div>
))}
</div>
</div>

{/* Buttons */}
<div className="flex flex-col items-center gap-2 w-full justify-between">
<Button
variant="outline"
onClick={handleResetFilters}
className={`${buttonStyle} bg-foreground text-background`}
>
Reset
</Button>
<Button
onClick={handleApplyFilters}
className={`${buttonStyle} bg-background text-foreground`}
>
Apply Filters
</Button>
</div>
</div>
)}
</main>
);
};

export default ProductFilter;
Loading

0 comments on commit 57f87ee

Please sign in to comment.