676 lines
28 KiB
TypeScript
676 lines
28 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import {
|
|
productService,
|
|
ClasseMercadologica,
|
|
} from "../src/services/product.service";
|
|
import { authService } from "../src/services/auth.service";
|
|
import { CustomAutocomplete } from "./ui/autocomplete";
|
|
|
|
interface DepartmentItem {
|
|
name: string;
|
|
url: string;
|
|
path: string; // Adicionar path para seguir padrão do Angular
|
|
subcategories: DepartmentItem[];
|
|
}
|
|
|
|
interface FilterSidebarProps {
|
|
selectedDepartment: string;
|
|
onDepartmentChange: (department: string) => void;
|
|
selectedBrands: string[];
|
|
onBrandsChange: (brands: string[]) => void;
|
|
filters: {
|
|
onlyInStock: boolean;
|
|
stockBranch: string;
|
|
onPromotion: boolean;
|
|
discountRange: [number, number];
|
|
priceDropped: boolean;
|
|
opportunities: boolean;
|
|
unmissableOffers: boolean;
|
|
};
|
|
onFiltersChange: (filters: FilterSidebarProps["filters"]) => void;
|
|
onApplyFilters: () => void;
|
|
brands?: string[]; // Marcas podem ser passadas como prop opcional
|
|
onBrandsLoaded?: (brands: string[]) => void; // Callback quando marcas são carregadas
|
|
}
|
|
|
|
const FilterSidebar: React.FC<FilterSidebarProps> = ({
|
|
selectedDepartment,
|
|
onDepartmentChange,
|
|
selectedBrands,
|
|
onBrandsChange,
|
|
filters,
|
|
onFiltersChange,
|
|
onApplyFilters,
|
|
brands: brandsProp,
|
|
onBrandsLoaded,
|
|
}) => {
|
|
const [expandedDepartments, setExpandedDepartments] = useState<string[]>([]);
|
|
const [departments, setDepartments] = useState<DepartmentItem[]>([]);
|
|
const [brands, setBrands] = useState<string[]>(brandsProp || []);
|
|
const [stores, setStores] = useState<
|
|
Array<{ id: string; name: string; shortName: string }>
|
|
>([]);
|
|
const [loadingDepartments, setLoadingDepartments] = useState<boolean>(true);
|
|
const [loadingBrands, setLoadingBrands] = useState<boolean>(false);
|
|
const [loadingStores, setLoadingStores] = useState<boolean>(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Sincronizar marcas quando a prop mudar
|
|
useEffect(() => {
|
|
if (brandsProp) {
|
|
setBrands(brandsProp);
|
|
if (onBrandsLoaded) {
|
|
onBrandsLoaded(brandsProp);
|
|
}
|
|
}
|
|
}, [brandsProp, onBrandsLoaded]);
|
|
|
|
/**
|
|
* Carrega as filiais disponíveis para o usuário
|
|
*/
|
|
useEffect(() => {
|
|
const loadStores = async () => {
|
|
try {
|
|
setLoadingStores(true);
|
|
const storesData = await productService.getStores();
|
|
setStores(storesData);
|
|
} catch (err: any) {
|
|
console.error("Erro ao carregar filiais:", err);
|
|
// Não define erro global pois as filiais são opcionais
|
|
} finally {
|
|
setLoadingStores(false);
|
|
}
|
|
};
|
|
|
|
loadStores();
|
|
}, []);
|
|
|
|
/**
|
|
* Mapeia os dados hierárquicos da API para a estrutura do componente
|
|
* Segue o mesmo padrão do Angular: mapItems
|
|
*/
|
|
/**
|
|
* Mapeia os dados hierárquicos da API para a estrutura do componente
|
|
* Segue EXATAMENTE o padrão do Angular: mapItems
|
|
* No Angular: path: item.url (cada item tem sua própria URL, usada diretamente)
|
|
*
|
|
* IMPORTANTE: No Angular, quando clica em uma seção dentro de um departamento,
|
|
* a URL da seção no banco já deve ser completa (ex: "ferragens/cadeados")
|
|
* ou precisa ser construída concatenando departamento/seção.
|
|
*
|
|
* Seguindo o exemplo do usuário "ferragens/cadeados", vamos construir o path
|
|
* completo quando há hierarquia, mas também manter a URL original do item.
|
|
*/
|
|
const mapItems = (
|
|
items: any[],
|
|
textFields: string[],
|
|
childFields: string[],
|
|
level: number = 0,
|
|
parentPath: string = "" // Path do item pai para construir URL completa quando necessário
|
|
): DepartmentItem[] => {
|
|
const childField = childFields[level];
|
|
const textField = textFields[level];
|
|
|
|
return items
|
|
.filter((item) => item && item[textField] != null)
|
|
.map((item) => {
|
|
// No Angular: path: item.url (usa diretamente a URL do item)
|
|
const itemUrl = item.url || "";
|
|
|
|
// Construir path completo seguindo hierarquia
|
|
// Se há parentPath e itemUrl não começa com parentPath, concatenar
|
|
// Caso contrário, usar apenas itemUrl (já vem completo da API)
|
|
let fullPath = itemUrl;
|
|
if (parentPath && itemUrl && !itemUrl.startsWith(parentPath)) {
|
|
// Se a URL não começa com o path do pai, construir concatenando
|
|
fullPath = `${parentPath}/${itemUrl}`;
|
|
} else if (!itemUrl && parentPath) {
|
|
// Se não tem URL mas tem parentPath, usar apenas parentPath
|
|
fullPath = parentPath;
|
|
}
|
|
|
|
const result: DepartmentItem = {
|
|
name: item[textField] || "",
|
|
url: itemUrl, // URL original do item
|
|
path: fullPath, // Path completo para usar no filtro (seguindo padrão do Angular)
|
|
subcategories: [],
|
|
};
|
|
|
|
if (
|
|
childField &&
|
|
item[childField] &&
|
|
Array.isArray(item[childField]) &&
|
|
item[childField].length > 0
|
|
) {
|
|
// Passar o path completo para os filhos (para construir hierarquia completa)
|
|
result.subcategories = mapItems(
|
|
item[childField],
|
|
textFields,
|
|
childFields,
|
|
level + 1,
|
|
fullPath // Passar path completo para construir hierarquia
|
|
);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Carrega os departamentos da API
|
|
*/
|
|
useEffect(() => {
|
|
const loadDepartments = async () => {
|
|
try {
|
|
setLoadingDepartments(true);
|
|
setError(null);
|
|
|
|
const data = await productService.getClasseMercadologica();
|
|
// Seguir EXATAMENTE o padrão do Angular: mapItems com os mesmos parâmetros
|
|
const mappedItems = mapItems(
|
|
data.filter((d) => d.tituloEcommerce != null), // Filtrar como no Angular
|
|
["tituloEcommerce", "descricaoSecao", "descricaoCategoria"],
|
|
["secoes", "categorias"],
|
|
0, // level inicial
|
|
"" // parentPath inicial vazio
|
|
);
|
|
|
|
setDepartments(mappedItems);
|
|
|
|
// Departamentos iniciam todos fechados (não expandidos)
|
|
} catch (err: any) {
|
|
console.error("Erro ao carregar departamentos:", err);
|
|
setError("Erro ao carregar departamentos. Tente novamente.");
|
|
} finally {
|
|
setLoadingDepartments(false);
|
|
}
|
|
};
|
|
|
|
loadDepartments();
|
|
}, []);
|
|
|
|
/**
|
|
* Carrega as marcas a partir dos produtos
|
|
* Segue o mesmo padrão do Angular: extrai marcas dos produtos retornados pela API
|
|
* No Angular: quando getProductByFilter é chamado, os produtos são processados para extrair marcas
|
|
*
|
|
* NOTA: As marcas serão carregadas quando os produtos forem buscados pela primeira vez
|
|
* Por enquanto, não carregamos automaticamente para evitar chamadas desnecessárias à API
|
|
* O componente pai pode passar as marcas via props quando necessário
|
|
*/
|
|
useEffect(() => {
|
|
// As marcas serão carregadas dinamicamente quando os produtos forem buscados
|
|
// Isso evita uma chamada extra à API no carregamento inicial
|
|
// O componente pai pode passar as marcas via props quando necessário
|
|
setLoadingBrands(false);
|
|
}, []);
|
|
|
|
return (
|
|
<aside className="w-full xl:w-80 bg-white border-r border-slate-200 p-4 xl:p-6 flex flex-col overflow-y-auto custom-scrollbar">
|
|
{/* Todos Departamentos */}
|
|
<div className="mb-8">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] mb-4">
|
|
Todos Departamentos
|
|
</h3>
|
|
{loadingDepartments ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-orange-500"></div>
|
|
<span className="ml-2 text-xs text-slate-500">Carregando...</span>
|
|
</div>
|
|
) : error ? (
|
|
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
<p className="text-xs text-red-600">{error}</p>
|
|
</div>
|
|
) : (
|
|
<nav className="space-y-0.5">
|
|
{departments.map((dept) => (
|
|
<div key={dept.name}>
|
|
<div className="flex items-center">
|
|
{/* Ícone para expandir/colapsar (apenas se tiver subcategorias) */}
|
|
{dept.subcategories.length > 0 && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setExpandedDepartments((prev) =>
|
|
prev.includes(dept.name)
|
|
? prev.filter((d) => d !== dept.name)
|
|
: [...prev, dept.name]
|
|
);
|
|
}}
|
|
className="p-1 hover:bg-slate-100 rounded transition-colors"
|
|
>
|
|
<svg
|
|
className={`w-3 h-3 transition-transform ${
|
|
expandedDepartments.includes(dept.name)
|
|
? "rotate-90"
|
|
: ""
|
|
}`}
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
{/* Espaçador quando não tem subcategorias */}
|
|
{dept.subcategories.length === 0 && (
|
|
<div className="w-5"></div>
|
|
)}
|
|
{/* Botão do texto para aplicar filtro */}
|
|
<button
|
|
onClick={() => {
|
|
// Sempre aplicar o filtro ao clicar no texto
|
|
// Seguir padrão do Angular: usar path (que é item.url)
|
|
// No Angular: this.filters.urlCategory = data.dataItem.path;
|
|
onDepartmentChange(dept.path || dept.url || dept.name);
|
|
// Se tiver subcategorias e não estiver expandido, expandir também
|
|
if (
|
|
dept.subcategories.length > 0 &&
|
|
!expandedDepartments.includes(dept.name)
|
|
) {
|
|
setExpandedDepartments((prev) => [...prev, dept.name]);
|
|
}
|
|
}}
|
|
className={`flex-1 text-left px-3 py-2 rounded-lg text-xs font-bold transition-all ${
|
|
selectedDepartment ===
|
|
(dept.path || dept.url || dept.name)
|
|
? "bg-blue-50 text-blue-600"
|
|
: "text-slate-600 hover:bg-slate-50"
|
|
}`}
|
|
>
|
|
{dept.name}
|
|
</button>
|
|
</div>
|
|
{dept.subcategories.length > 0 &&
|
|
expandedDepartments.includes(dept.name) && (
|
|
<div className="ml-5 mt-0.5 space-y-0.5">
|
|
{dept.subcategories.map((sub) => {
|
|
const hasSubcategories =
|
|
sub.subcategories && sub.subcategories.length > 0;
|
|
const isSubExpanded = expandedDepartments.includes(
|
|
`${dept.name}-${sub.name}`
|
|
);
|
|
|
|
return (
|
|
<div key={sub.url || sub.name}>
|
|
<div className="flex items-center">
|
|
{/* Ícone para expandir/colapsar (apenas se tiver subcategorias) */}
|
|
{hasSubcategories && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setExpandedDepartments((prev) =>
|
|
isSubExpanded
|
|
? prev.filter(
|
|
(d) =>
|
|
d !== `${dept.name}-${sub.name}`
|
|
)
|
|
: [...prev, `${dept.name}-${sub.name}`]
|
|
);
|
|
}}
|
|
className="p-1 hover:bg-slate-100 rounded transition-colors"
|
|
>
|
|
<svg
|
|
className={`w-3 h-3 transition-transform ${
|
|
isSubExpanded ? "rotate-90" : ""
|
|
}`}
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
{/* Espaçador quando não tem subcategorias */}
|
|
{!hasSubcategories && <div className="w-5"></div>}
|
|
{/* Botão do texto para aplicar filtro */}
|
|
<button
|
|
onClick={() => {
|
|
// Sempre aplicar o filtro ao clicar no texto
|
|
// Seguir EXATAMENTE o padrão do Angular: usar path (que já é a URL completa)
|
|
// No Angular: this.filters.urlCategory = data.dataItem.path;
|
|
// O path já foi construído no mapItems com a hierarquia completa
|
|
onDepartmentChange(
|
|
sub.path || sub.url || sub.name
|
|
);
|
|
// Se tiver subcategorias e não estiver expandido, expandir também
|
|
if (hasSubcategories && !isSubExpanded) {
|
|
setExpandedDepartments((prev) => [
|
|
...prev,
|
|
`${dept.name}-${sub.name}`,
|
|
]);
|
|
}
|
|
}}
|
|
className={`flex-1 text-left px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
|
selectedDepartment ===
|
|
(sub.path || sub.url || sub.name)
|
|
? "bg-blue-50 text-blue-600"
|
|
: "text-slate-500 hover:bg-slate-50"
|
|
}`}
|
|
>
|
|
{sub.name}
|
|
</button>
|
|
</div>
|
|
{/* Renderizar categorias (terceiro nível) se existirem */}
|
|
{hasSubcategories && isSubExpanded && (
|
|
<div className="ml-5 mt-0.5 space-y-0.5">
|
|
{sub.subcategories.map((category) => (
|
|
<button
|
|
key={category.url || category.name}
|
|
onClick={() => {
|
|
// Seguir EXATAMENTE o padrão do Angular: usar path (que já é a URL completa)
|
|
// No Angular: this.filters.urlCategory = data.dataItem.path;
|
|
onDepartmentChange(
|
|
category.path ||
|
|
category.url ||
|
|
category.name
|
|
);
|
|
}}
|
|
className={`w-full text-left px-3 py-1.5 rounded-lg text-xs font-medium transition-all flex items-center ${
|
|
selectedDepartment ===
|
|
(category.path ||
|
|
category.url ||
|
|
category.name)
|
|
? "bg-blue-50 text-blue-600"
|
|
: "text-slate-400 hover:bg-slate-50"
|
|
}`}
|
|
>
|
|
<svg
|
|
className="w-3 h-3 mr-2"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
{category.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
)}
|
|
</div>
|
|
|
|
{/* Marcas */}
|
|
<div className="mb-8">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] mb-4">
|
|
Marcas
|
|
</h3>
|
|
{loadingBrands ? (
|
|
<div className="flex items-center justify-center py-4">
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-orange-500"></div>
|
|
<span className="ml-2 text-xs text-slate-500">Carregando...</span>
|
|
</div>
|
|
) : brands.length === 0 ? (
|
|
<p className="text-xs text-slate-400 italic">
|
|
Nenhuma marca disponível
|
|
</p>
|
|
) : (
|
|
<div className="space-y-1.5 max-h-[300px] overflow-y-auto custom-scrollbar">
|
|
{brands.map((brand) => (
|
|
<label
|
|
key={brand}
|
|
className="flex items-center px-3 py-2 rounded-lg hover:bg-slate-50 cursor-pointer group"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedBrands.includes(brand)}
|
|
onChange={(e) => {
|
|
if (e.target.checked) {
|
|
onBrandsChange([...selectedBrands, brand]);
|
|
} else {
|
|
onBrandsChange(selectedBrands.filter((b) => b !== brand));
|
|
}
|
|
}}
|
|
className="w-4 h-4 border-2 border-slate-300 rounded text-orange-500 focus:ring-2 focus:ring-orange-500/20 cursor-pointer"
|
|
/>
|
|
<span className="ml-3 text-xs font-medium text-slate-600 group-hover:text-slate-900">
|
|
{brand}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Filtros */}
|
|
<div className="mb-8">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] mb-4">
|
|
Filtros
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{/* Somente produtos com estoque */}
|
|
<label className="flex items-center cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
checked={filters.onlyInStock}
|
|
onChange={(e) =>
|
|
onFiltersChange({ ...filters, onlyInStock: e.target.checked })
|
|
}
|
|
className="w-4 h-4 border-2 border-slate-300 rounded text-orange-500 focus:ring-2 focus:ring-orange-500/20 cursor-pointer"
|
|
/>
|
|
<span className="ml-3 text-xs font-medium text-slate-600 group-hover:text-slate-900">
|
|
Somente produtos com estoque
|
|
</span>
|
|
</label>
|
|
|
|
{/* Filial de estoque */}
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-600 mb-1.5">
|
|
Filial de estoque
|
|
</label>
|
|
{loadingStores ? (
|
|
<div className="flex items-center justify-center py-2">
|
|
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-orange-500"></div>
|
|
<span className="ml-2 text-xs text-slate-400">
|
|
Carregando...
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<CustomAutocomplete
|
|
options={[
|
|
{ value: "", label: "Selecione a filial" },
|
|
...stores.map((store) => ({
|
|
value: store.id,
|
|
label: store.shortName || store.name || store.id,
|
|
})),
|
|
]}
|
|
value={filters.stockBranch}
|
|
onValueChange={(value) =>
|
|
onFiltersChange({ ...filters, stockBranch: value })
|
|
}
|
|
placeholder="Selecione a filial"
|
|
className="h-9 text-xs"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Produtos em promoção */}
|
|
<label className="flex items-center cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
checked={filters.onPromotion}
|
|
onChange={(e) =>
|
|
onFiltersChange({ ...filters, onPromotion: e.target.checked })
|
|
}
|
|
className="w-4 h-4 border-2 border-slate-300 rounded text-orange-500 focus:ring-2 focus:ring-orange-500/20 cursor-pointer"
|
|
/>
|
|
<span className="ml-3 text-xs font-medium text-slate-600 group-hover:text-slate-900">
|
|
Produtos em promoção
|
|
</span>
|
|
</label>
|
|
|
|
{/* Faixa de desconto */}
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-600 mb-2">
|
|
Faixa de desconto
|
|
</label>
|
|
<div className="px-1">
|
|
<div className="relative h-2 bg-slate-200 rounded-full mb-3">
|
|
{/* Barra de progresso visual */}
|
|
<div
|
|
className="absolute h-2 bg-orange-500 rounded-full"
|
|
style={{
|
|
left: `${filters.discountRange[0]}%`,
|
|
width: `${
|
|
filters.discountRange[1] - filters.discountRange[0]
|
|
}%`,
|
|
}}
|
|
></div>
|
|
{/* Input mínimo */}
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="100"
|
|
step="5"
|
|
value={filters.discountRange[0]}
|
|
onChange={(e) => {
|
|
const min = parseInt(e.target.value);
|
|
const max = Math.max(min, filters.discountRange[1]);
|
|
onFiltersChange({ ...filters, discountRange: [min, max] });
|
|
}}
|
|
className="absolute w-full h-2 bg-transparent appearance-none cursor-pointer z-10 opacity-0"
|
|
style={{
|
|
background: "transparent",
|
|
}}
|
|
/>
|
|
{/* Input máximo */}
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="100"
|
|
step="5"
|
|
value={filters.discountRange[1]}
|
|
onChange={(e) => {
|
|
const max = parseInt(e.target.value);
|
|
const min = Math.min(max, filters.discountRange[0]);
|
|
onFiltersChange({ ...filters, discountRange: [min, max] });
|
|
}}
|
|
className="absolute w-full h-2 bg-transparent appearance-none cursor-pointer z-10 opacity-0"
|
|
/>
|
|
{/* Handles visuais */}
|
|
<div
|
|
className="absolute w-4 h-4 bg-orange-500 rounded-full border-2 border-white shadow-md cursor-grab active:cursor-grabbing z-20 top-1/2 -translate-y-1/2 hover:scale-110 transition-transform"
|
|
style={{
|
|
left: `calc(${filters.discountRange[0]}% - 8px)`,
|
|
}}
|
|
></div>
|
|
<div
|
|
className="absolute w-4 h-4 bg-orange-500 rounded-full border-2 border-white shadow-md cursor-grab active:cursor-grabbing z-20 top-1/2 -translate-y-1/2 hover:scale-110 transition-transform"
|
|
style={{
|
|
left: `calc(${filters.discountRange[1]}% - 8px)`,
|
|
}}
|
|
></div>
|
|
</div>
|
|
<div className="flex justify-between mb-2 text-[10px] text-slate-400">
|
|
<span>0</span>
|
|
<span>20</span>
|
|
<span>40</span>
|
|
<span>60</span>
|
|
<span>80</span>
|
|
<span>100</span>
|
|
</div>
|
|
<div className="text-center text-xs font-medium text-slate-700">
|
|
{filters.discountRange[0]}% - {filters.discountRange[1]}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Produtos que baixaram de preço */}
|
|
<label className="flex items-center cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
checked={filters.priceDropped}
|
|
onChange={(e) =>
|
|
onFiltersChange({ ...filters, priceDropped: e.target.checked })
|
|
}
|
|
className="w-4 h-4 border-2 border-slate-300 rounded text-orange-500 focus:ring-2 focus:ring-orange-500/20 cursor-pointer"
|
|
/>
|
|
<span className="ml-3 text-xs font-medium text-slate-600 group-hover:text-slate-900">
|
|
Produtos que baixaram de preço
|
|
</span>
|
|
</label>
|
|
|
|
{/* Oportunidades */}
|
|
<label className="flex items-center cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
checked={filters.opportunities}
|
|
onChange={(e) =>
|
|
onFiltersChange({ ...filters, opportunities: e.target.checked })
|
|
}
|
|
className="w-4 h-4 border-2 border-slate-300 rounded text-orange-500 focus:ring-2 focus:ring-orange-500/20 cursor-pointer"
|
|
/>
|
|
<span className="ml-3 text-xs font-medium text-slate-600 group-hover:text-slate-900">
|
|
Oportunidades
|
|
</span>
|
|
</label>
|
|
|
|
{/* Ofertas imperdiveis */}
|
|
<label className="flex items-center cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
checked={filters.unmissableOffers}
|
|
onChange={(e) =>
|
|
onFiltersChange({
|
|
...filters,
|
|
unmissableOffers: e.target.checked,
|
|
})
|
|
}
|
|
className="w-4 h-4 border-2 border-slate-300 rounded text-orange-500 focus:ring-2 focus:ring-orange-500/20 cursor-pointer"
|
|
/>
|
|
<span className="ml-3 text-xs font-medium text-slate-600 group-hover:text-slate-900">
|
|
Ofertas imperdiveis
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Botão Aplicar Filtros */}
|
|
<button
|
|
onClick={onApplyFilters}
|
|
className="w-full py-3 lg:py-3 bg-orange-500 text-white rounded-lg font-bold text-xs uppercase tracking-wider hover:bg-orange-600 transition-all shadow-lg shadow-orange-500/20 touch-manipulation"
|
|
>
|
|
Aplicar Filtros
|
|
</button>
|
|
|
|
{/* Informação da Loja */}
|
|
<div className="mt-auto pt-6 border-t border-slate-200">
|
|
<div className="bg-slate-50 rounded-xl p-4 border border-slate-100">
|
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 block">
|
|
Sua Loja
|
|
</span>
|
|
<span className="text-sm font-extrabold text-[#002147]">
|
|
Jurunense BR-316
|
|
</span>
|
|
<div className="mt-3 flex items-center text-xs font-bold text-emerald-600">
|
|
<span className="w-2 h-2 bg-emerald-500 rounded-full mr-2"></span>
|
|
Loja aberta agora
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
};
|
|
|
|
export default FilterSidebar;
|