Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add product filters #141

Merged
merged 13 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading