I’m creating a product page where users can filter items based on various variants like “Size”, “Color” etc. However, I am facing an issue when trying to filter product categories based on selected variants.
Here’s what I want to achieve:
If the user selects size “P”, they should continue to see other size variants (M, L, XL), but categories that do not have products with size “P” should be removed from the filter list.
If the user selects another category, for example “Pants”, filtering logic must be applied so that the other categories are adjusted to show only those that have products with size “P” or another selected size.
The behavior is the same for other variants, such as “Color” or “Gender” — the idea is that selecting a variant influences which categories appear, without removing categories with other variants still available.
Here is the code I have already implemented to search for available products and filters:
Route.js:
const filters = {};
if (genders.length) {
filters.variants = { some: { gender: { in: genders } } };
}
if (colors.length) {
if (!filters.variants) filters.variants = {};
filters.variants.some = { ...filters.variants.some, color: { in: colors } };
}
if (sizes.length) {
if (!filters.variants) filters.variants = {};
filters.variants.some = { ...filters.variants.some, size: { in: sizes } };
}
if (category.length) {
filters.categories = { some: { Category: { name: { in: category } } } };
}
const availableFilters = {
genders: await prisma.productVariant.findMany({
select: { gender: true },
distinct: ["gender"],
where: {
product: { AND: filters },
},
}),
sizes: await prisma.productVariant.findMany({
select: { size: true },
distinct: ["size"],
where: {
product: { AND: filters },
},
}),
colors: await prisma.productVariant.findMany({
select: { color: true },
distinct: ["color"],
where: {
product: { AND: filters },
},
}),
categories: await prisma.category.findMany({
where: {
products: {
some: {
product: { AND: filters },
},
},
},
select: { name: true },
}),
};
ProductsPage:
"use client";
import Filtro from "@/components/Produtos/Filtro";
import BotãoOrdenar from "./BotãoOrdenar";
import { useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import ProdutoCard from "./ProdutoCard";
import BotãoFiltro from "./BotãoFiltro";
export default function PaginaProdutos({ titulo }) {
const searchParams = useSearchParams();
const [isLoading, setIsLoading] = useState(true);
const [produtos, setProdutos] = useState([]);
const [availableFilters, setAvailableFilters] = useState({
genders: [],
sizes: [],
colors: [],
categories: [],
});
const tamanhoOrdem = ["PP", "P", "M", "G", "GG"];
function ordenarTamanhos(tamanhos) {
return tamanhos.sort((a, b) => {
const indexA = tamanhoOrdem.indexOf(a.size);
const indexB = tamanhoOrdem.indexOf(b.size);
return indexA - indexB;
});
}
useEffect(() => {
async function fetchProducts() {
setIsLoading(true);
try {
const params = new URLSearchParams(searchParams.toString());
params.append("pathname", window.location.pathname);
const response = await fetch(`/api/produtos?${params}`);
if (response.ok) {
const { produtos, availableFilters } = await response.json();
setProdutos(produtos);
setAvailableFilters({
...availableFilters,
sizes: ordenarTamanhos(availableFilters.sizes),
});
} else {
console.error("Error ao buscar produtos");
}
} catch (err) {
console.error("Erro na requisição", err);
} finally {
setIsLoading(false);
}
}
fetchProducts();
}, [searchParams]);
return (
<div className="font-raleway text-text">
<div className="flex items-center text-center min-[1001px]:mb-[40px] mt-[20px] px-5">
<h1 className="flex-1 font-bold text-[32px] max-[1340px]:text-[30px] max-[1000px]:text-[28px] max-[550px]:text-[25px]">
{titulo}
</h1>
<BotãoFiltro filters={availableFilters} />
</div>
<div className="max-[850px]:px-[15px] px-[50px]">
<BotãoOrdenar />
{isLoading ? (
<div className="h-[40vh] w-full text-center flex justify-center items-center flex-col gap-2">
<span className="w-[48px] h-[48px] border-[5px] border-icon border-b-transparent rounded-[50%] animate-spin flex" />
<span className="text-icon font-semibold text-[18px]">
CARREGANDO
</span>
</div>
) : (
<div className="flex gap-[30px] pt-5 mb-5 max-[1000px]:pt-10">
<Filtro filters={availableFilters} />
<ul className="flex flex-wrap max-[800px]:gap-y-2.5 max-[800px]:gap-x-2.5 gap-x-5 gap-y-5 list-none max-[1000px]:w-full w-[calc(100%-320px)]">
{produtos.map((p) => (
<ProdutoCard
key={p.id}
nome={p.name}
slug={p.slug}
images={p.images}
/>
))}
</ul>
</div>
)}
</div>
</div>
);
}
Database:
The data structure of my database is as follows (with the main related tables: Product, ProductVariant, Category):
model Product {
id Int @id @default(autoincrement())
name String
variants ProductVariant[]
categories ProductCategory[]
}
model ProductVariant {
id Int @id @default(autoincrement())
color String
gender String
size Size
productId Int
product Product @relation(fields: [productId], references: [id])
}
model Category {
id Int @id @default(autoincrement())
name String @unique
products ProductCategory[]
}
What I’m trying to do:
- I want to apply category filtering based on selected variants such as Size, Color and Gender dynamically. That is, if the user selects a specific size, for example “P”, only categories that have that size should be shown.
- Categories should not be removed if there are still products with other variants (such as different sizes, for example “M” or “L”).
- The problem is that the category filtering is excluding categories with products in other variants, which is not what I want.
Doubts:
- How can I adjust my query in route.js so that categories are not removed if there are still products in other variants?
- Is there an efficient way to ensure that only categories with the selected variants are shown, without removing the other options?