Product Filters

PreviousNext

Filter products by name or category with smooth animated transitions.

Docs
paceuiblock

Preview

Loading preview…
product-filter/product-filter-1.tsx
"use client";

import { PlusIcon, XCircleIcon } from "lucide-react";
import { useCallback, useMemo, useState } from "react";

import { FlipReveal, FlipRevealItem } from "@/components/gsap/flip-reveal";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
    Select,
    SelectContent,
    SelectGroup,
    SelectItem,
    SelectLabel,
    SelectTrigger,
    SelectValue,
} from "@/components/ui/select";

type IProduct = {
    sku: string;
    image: string;
    name: string;
    category: string;
    price: number;
};

const products: IProduct[] = [
    {
        sku: "TEE-WHT-CLSC",
        image: "https://images.unsplash.com/photo-1696086152504-4843b2106ab4?q=80&w=300",
        name: "Classic White Tee",
        category: "Apparel",
        price: 20,
    },
    {
        sku: "GLS-WHT-RETRO",
        image: "https://images.unsplash.com/photo-1648688135643-2716ec8f4b24?q=80&w=300",
        name: "Retro White Shades",
        category: "Eyewear",
        price: 30,
    },
    {
        sku: "SHO-TAN-STRAP",
        image: "https://images.unsplash.com/photo-1631984564919-1f6b2313a71c?q=80&w=300",
        name: "Tan Strap Sneakers",
        category: "Footwear",
        price: 65,
    },
    {
        sku: "GLS-AVIATOR",
        image: "https://images.unsplash.com/photo-1632168844625-b22d7b1053c0?q=80&w=300",
        name: "Aviator Style Sunglasses",
        category: "Eyewear",
        price: 40,
    },
    {
        sku: "TEE-BLK-JET",
        image: "https://images.unsplash.com/photo-1583656346517-4716a62e27b7?q=80&w=300",
        name: "Jet Black Tee",
        category: "Apparel",
        price: 22,
    },
    {
        sku: "RUN-WHT-SLEEK",
        image: "https://images.unsplash.com/photo-1596480370804-cff0eed14888?q=80&w=300",
        name: "Sleek White Runners",
        category: "Footwear",
        price: 75,
    },
    {
        sku: "SHIRT-BLU-CASL",
        image: "https://images.unsplash.com/photo-1740711152088-88a009e877bb?q=80&w=300",
        name: "Casual Blue Shirt",
        category: "Apparel",
        price: 28,
    },
    {
        sku: "SNKRS-EVRDY",
        image: "https://images.unsplash.com/photo-1696086152508-1711cc7bcc9d?q=80&w=300",
        name: "Everyday Sneakers",
        category: "Footwear",
        price: 60,
    },
    {
        sku: "GLS-CRYSTAL",
        image: "https://images.unsplash.com/photo-1684790369514-f292d2dffc11?q=80&w=300",
        name: "Crystal Frame Glasses",
        category: "Eyewear",
        price: 35,
    },
    {
        sku: "DRS-RED-ELEGNT",
        image: "https://images.unsplash.com/photo-1589810635657-232948472d98?q=80&w=300",
        name: "Elegant Red Dress",
        category: "Apparel",
        price: 50,
    },
    {
        sku: "LGG-RED-YOGA",
        image: "https://images.unsplash.com/photo-1590159983013-d4ff5fc71c1d?q=80&w=300",
        name: "Red Yoga Leggings",
        category: "Activewear",
        price: 35,
    },
    {
        sku: "HPHN-PNK-BLSH",
        image: "https://images.unsplash.com/photo-1617460237920-ea0b1bad4b0a?q=80&w=300",
        name: "Blush Pink Headphones",
        category: "Audio",
        price: 90,
    },
    {
        sku: "EARBDS-WIRELS",
        image: "https://images.unsplash.com/photo-1614288064424-11d2d386c474?q=80&w=300",
        name: "Wireless Earbuds",
        category: "Audio",
        price: 60,
    },
    {
        sku: "WTCH-BRN-ANLG",
        image: "https://images.unsplash.com/photo-1650779456233-fc579766337c?q=80&w=300",
        name: "Analog Leather Watch",
        category: "Accessories",
        price: 110,
    },
    {
        sku: "WTCH-APL-SMRT",
        image: "https://images.unsplash.com/photo-1704942968209-6c1e05ef3f95?q=80&w=300",
        name: "Smart Fitness Watch",
        category: "Accessories",
        price: 150,
    },
];

const categories = [...new Set(products.map((item) => item.category))];

const ProductFilter1 = () => {
    const [filter, setFilter] = useState({
        search: "",
        category: "",
    });

    const isEligible = useCallback(
        (product: IProduct) => {
            const search = filter.search?.toLowerCase() ?? "";
            const category = filter.category;

            const matchesSearch = search.length === 0 || product.name.toLowerCase().includes(search);
            const matchesCategory = !category || product.category === category;

            return matchesSearch && matchesCategory;
        },
        [filter],
    );

    const visibleProductKeys = useMemo(() => {
        if (!filter.search && !filter.category) return ["all"];

        return products.filter(isEligible).map((product) => product.sku);
    }, [filter.category, filter.search, isEligible]);

    return (
        <div className="bg-background container min-h-280 py-4 sm:py-8 xl:py-16">
            <div className="flex flex-col items-center">
                <h2 className="text-2xl font-medium sm:text-3xl lg:text-4xl">Browse Products</h2>
                <p className="text-muted-foreground mt-1 max-w-md text-center max-sm:text-sm">
                    Find what you need by filtering through our selection of items across categories.
                </p>
            </div>
            <div className="mt-4 flex gap-4 rounded-md sm:mt-8 lg:mt-12">
                <div className="max-lg:w-1/2 lg:w-50">
                    <Label>Search</Label>
                    <Input
                        placeholder="Search by name"
                        value={filter.search}
                        className="w-full"
                        autoFocus
                        onChange={(e) => setFilter({ ...filter, search: e.target.value })}
                    />
                </div>
                <div className="max-lg:w-1/2 lg:w-50">
                    <Label>Category</Label>
                    <Select value={filter.category} onValueChange={(e) => setFilter({ ...filter, category: e })}>
                        <SelectTrigger className="w-full">
                            <SelectValue placeholder="Select a category" />
                        </SelectTrigger>
                        <SelectContent>
                            <SelectGroup>
                                <SelectLabel>Category</SelectLabel>
                                {categories.map((item, index) => (
                                    <SelectItem value={item} key={index}>
                                        {item}
                                    </SelectItem>
                                ))}
                            </SelectGroup>
                        </SelectContent>
                    </Select>
                </div>
            </div>
            <div className="mt-4 lg:mt-6">
                <div className="flex items-center justify-between gap-2 max-sm:text-sm">
                    <span className="">Here’s What We Found...</span>
                    {visibleProductKeys[0] !== "all" && (
                        <button
                            className="text-muted-foreground hover:text-foreground flex cursor-pointer items-center gap-1.5"
                            onClick={() => setFilter({ search: "", category: "" })}>
                            <XCircleIcon className="size-3.5" />
                            <span>Clear filter</span>
                        </button>
                    )}
                </div>
                {visibleProductKeys[0] === "all" || visibleProductKeys.length > 0 ? (
                    <FlipReveal
                        className="mt-3 grid grid-cols-2 gap-x-4 gap-y-6 sm:gap-x-6 sm:gap-y-8 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-5"
                        keys={visibleProductKeys}
                        showClass="flex"
                        hideClass="hidden">
                        {products.map((product) => (
                            <FlipRevealItem flipKey={product.sku} key={product.sku}>
                                <div className="group relative w-full cursor-pointer">
                                    <div className="bg-muted absolute -inset-2 scale-50 rounded-md opacity-0 transition-all group-hover:scale-100 group-hover:opacity-100"></div>

                                    <div className="relative">
                                        <div className="relative">
                                            <img
                                                src={product.image}
                                                alt={product.name}
                                                className="h-32 w-full rounded-md object-cover sm:h-40 xl:h-50"
                                            />
                                            <div className="bg-background absolute end-1 bottom-1 z-1 flex items-center gap-0.5 rounded px-1 py-0.5 text-xs opacity-0 transition-all group-hover:opacity-100">
                                                <PlusIcon className="size-3" />
                                                Add Item
                                            </div>
                                        </div>
                                        <div className="mt-3 flex items-end justify-between gap-1">
                                            <div>
                                                <p className="text-muted-foreground font-mono text-[10px]/none tracking-wider uppercase">
                                                    {product.category}
                                                </p>
                                                <p className="mt-1 line-clamp-1 leading-none font-medium max-sm:text-sm">
                                                    {product.name}
                                                </p>
                                            </div>
                                            <p>
                                                <sup className="text-muted-foreground">$</sup>
                                                <span className="text-xl font-medium">{product.price}</span>
                                            </p>
                                        </div>
                                    </div>
                                </div>
                            </FlipRevealItem>
                        ))}
                    </FlipReveal>
                ) : (
                    <div className="text-muted-foreground mt-12 text-center">No products found</div>
                )}
            </div>
        </div>
    );
};

export default ProductFilter1;

Installation

npx shadcn@latest add @paceui/product-filter-1

Usage

import { ProductFilter1 } from "@/components/product-filter-1"
<ProductFilter1 />