466 lines
25 KiB
TypeScript
466 lines
25 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import {
|
|
Filter,
|
|
X,
|
|
AlertTriangle,
|
|
Zap,
|
|
Grid,
|
|
List,
|
|
RefreshCw,
|
|
ArrowUpDown,
|
|
LayoutGrid,
|
|
Tag,
|
|
Beaker,
|
|
BarChart3,
|
|
} from "lucide-react";
|
|
import * as LucideIcons from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Slider } from "@/components/ui/slider";
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from "@/components/ui/accordion";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { SortDropdown } from "@/app/components/SortDropdown";
|
|
import { FilterBadges } from "@/app/components/FilterBadge";
|
|
import {
|
|
ProductFilters,
|
|
NUTRITION_GRADES,
|
|
NOVA_GROUPS,
|
|
POPULAR_CATEGORIES_WITH_ICONS,
|
|
COMMON_ALLERGENS,
|
|
} from "@/lib/types";
|
|
|
|
interface FilterSidebarProps {
|
|
filters: ProductFilters;
|
|
onFiltersChange: (filters: ProductFilters) => void;
|
|
onClearFilters: () => void;
|
|
isLoading?: boolean;
|
|
totalResults?: number;
|
|
className?: string;
|
|
sortBy?: string;
|
|
onSortChange?: (sortBy: string) => void;
|
|
viewMode?: "grid" | "list";
|
|
onViewModeChange?: (mode: "grid" | "list") => void;
|
|
onRefresh?: () => void;
|
|
onClearCache?: () => void;
|
|
isCached?: boolean;
|
|
onRemoveFilter?: (key: string, value?: string) => void;
|
|
}
|
|
|
|
export function FilterSidebar({
|
|
filters,
|
|
onFiltersChange,
|
|
onClearFilters,
|
|
isLoading = false,
|
|
totalResults = 0,
|
|
className = "",
|
|
sortBy = "relevance",
|
|
onSortChange,
|
|
viewMode = "grid",
|
|
onViewModeChange,
|
|
onRefresh,
|
|
onClearCache,
|
|
isCached = false,
|
|
onRemoveFilter,
|
|
}: FilterSidebarProps) {
|
|
const [searchBrands, setSearchBrands] = useState("");
|
|
const [searchCategories, setSearchCategories] = useState("");
|
|
const [searchAllergens, setSearchAllergens] = useState("");
|
|
|
|
const updateFilter = (key: keyof ProductFilters, value: unknown) => {
|
|
onFiltersChange({ ...filters, [key]: value });
|
|
};
|
|
|
|
const updateArrayFilter = (
|
|
key: keyof ProductFilters,
|
|
value: string,
|
|
checked: boolean
|
|
) => {
|
|
const currentArray = (filters[key] as string[]) || [];
|
|
const newArray = checked
|
|
? [...currentArray, value]
|
|
: currentArray.filter((item) => item !== value);
|
|
updateFilter(key, newArray);
|
|
};
|
|
|
|
const updateNutritionRange = (
|
|
nutrient: string,
|
|
type: "min" | "max",
|
|
value: number
|
|
) => {
|
|
const currentRanges = filters.nutritionRanges || {};
|
|
const currentRange =
|
|
currentRanges[nutrient as keyof typeof currentRanges] || {};
|
|
|
|
updateFilter("nutritionRanges", {
|
|
...currentRanges,
|
|
[nutrient]: {
|
|
...currentRange,
|
|
[type]: value,
|
|
},
|
|
});
|
|
};
|
|
|
|
const hasActiveFilters = Object.values(filters).some((value) => {
|
|
if (Array.isArray(value)) return value.length > 0;
|
|
if (typeof value === "object" && value !== null) {
|
|
return Object.values(value).some((v) => v !== undefined && v !== null);
|
|
}
|
|
return value !== undefined && value !== null && value !== "";
|
|
});
|
|
|
|
return (
|
|
<div className={`w-full space-y-5 ${className}`}>
|
|
|
|
{/* ── Header ────────────────────────────────────────────────────────── */}
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", paddingBottom: 12, borderBottom: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<Filter style={{ width: 15, height: 15, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
|
<span style={{ fontFamily: "ui-monospace, monospace", fontSize: 10, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--fg-muted, #9a8870)", fontWeight: 600 }}>
|
|
Filtros avançados
|
|
</span>
|
|
</div>
|
|
{hasActiveFilters && (
|
|
<button
|
|
onClick={onClearFilters}
|
|
style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11, color: "var(--accent, oklch(52% 0.11 155))", background: "none", border: "none", cursor: "pointer", fontFamily: "inherit", letterSpacing: "0.03em" }}
|
|
>
|
|
<X style={{ width: 12, height: 12 }} strokeWidth={2} />
|
|
Limpar tudo
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* ── Result count ──────────────────────────────────────────────────── */}
|
|
{totalResults > 0 && (
|
|
<div style={{ fontFamily: "ui-monospace, monospace", fontSize: 11, color: "var(--fg-muted, #9a8870)", letterSpacing: "0.04em" }}>
|
|
{totalResults.toLocaleString()} produto{totalResults !== 1 ? "s" : ""} encontrado{totalResults !== 1 ? "s" : ""}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Active filter badges ───────────────────────────────────────────── */}
|
|
{onRemoveFilter && hasActiveFilters && (
|
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
|
<FilterBadges
|
|
filters={filters}
|
|
onRemoveFilter={onRemoveFilter}
|
|
onClearAll={onClearFilters}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Cache / Refresh controls ──────────────────────────────────────── */}
|
|
{(onRefresh || onClearCache) && (
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
{onRefresh && (
|
|
<button
|
|
onClick={onRefresh}
|
|
disabled={isLoading}
|
|
style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "7px 0", border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 8, background: "transparent", fontSize: 12, color: "var(--fg-secondary, #5e4e3c)", fontFamily: "inherit", cursor: isLoading ? "not-allowed" : "pointer", opacity: isLoading ? 0.5 : 1 }}
|
|
>
|
|
<RefreshCw style={{ width: 12, height: 12 }} className={isLoading ? "animate-spin" : ""} />
|
|
Atualizar
|
|
</button>
|
|
)}
|
|
{onClearCache && isCached && (
|
|
<button
|
|
onClick={onClearCache}
|
|
style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "7px 0", border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 8, background: "transparent", fontSize: 12, color: "var(--fg-secondary, #5e4e3c)", fontFamily: "inherit", cursor: "pointer" }}
|
|
>
|
|
<X style={{ width: 12, height: 12 }} />
|
|
Limpar cache
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── View mode ─────────────────────────────────────────────────────── */}
|
|
{onViewModeChange && (
|
|
<div>
|
|
<div style={{ fontFamily: "ui-monospace, monospace", fontSize: 10, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--fg-muted, #9a8870)", marginBottom: 8 }}>Visualização</div>
|
|
<div style={{ display: "flex", border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 8, overflow: "hidden" }}>
|
|
{(["grid", "list"] as const).map((mode) => (
|
|
<button
|
|
key={mode}
|
|
onClick={() => onViewModeChange(mode)}
|
|
style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "7px 0", background: viewMode === mode ? "var(--accent-muted, oklch(92% 0.04 155))" : "transparent", color: viewMode === mode ? "var(--accent, oklch(52% 0.11 155))" : "var(--fg-muted, #9a8870)", border: "none", fontSize: 12, cursor: "pointer", fontFamily: "inherit", fontWeight: viewMode === mode ? 500 : 400 }}
|
|
>
|
|
{mode === "grid" ? <Grid style={{ width: 13, height: 13 }} /> : <List style={{ width: 13, height: 13 }} />}
|
|
{mode === "grid" ? "Grade" : "Lista"}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Accordion sections ────────────────────────────────────────────── */}
|
|
<Accordion type="multiple" defaultValue={["nutrition"]} className="space-y-1">
|
|
|
|
{/* Ordenação */}
|
|
{onSortChange && (
|
|
<AccordionItem value="sorting" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
|
<AccordionTrigger className="px-4 py-3 hover:no-underline" style={{ background: "transparent" }}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<ArrowUpDown style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
|
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Ordenação</span>
|
|
</div>
|
|
</AccordionTrigger>
|
|
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
|
<div style={{ paddingTop: 12 }}>
|
|
<SortDropdown value={sortBy} onValueChange={onSortChange} className="w-full" />
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
)}
|
|
|
|
{/* Nutri-Score & NOVA */}
|
|
<AccordionItem value="nutrition" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
|
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<Zap style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
|
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Classificação Nutricional</span>
|
|
</div>
|
|
</AccordionTrigger>
|
|
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
|
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 14 }}>
|
|
<div>
|
|
<div style={{ fontFamily: "ui-monospace, monospace", fontSize: 10, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--fg-muted, #9a8870)", marginBottom: 10 }}>Nutri-Score</div>
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
{NUTRITION_GRADES.map((grade) => (
|
|
<div key={grade.grade} style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
<Checkbox
|
|
id={`nutrition-${grade.grade}`}
|
|
checked={filters.nutritionGrades?.includes(grade.grade) || false}
|
|
onCheckedChange={(checked) => updateArrayFilter("nutritionGrades", grade.grade, checked as boolean)}
|
|
/>
|
|
<label htmlFor={`nutrition-${grade.grade}`} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer", flex: 1 }}>
|
|
<Badge className={`${grade.color} text-white text-xs`}>{grade.label}</Badge>
|
|
<span style={{ color: "var(--fg-secondary, #5e4e3c)", fontSize: 12 }}>{grade.description}</span>
|
|
</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ height: 1, background: "var(--border-subtle, #e8e0d0)" }} />
|
|
|
|
<div>
|
|
<div style={{ fontFamily: "ui-monospace, monospace", fontSize: 10, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--fg-muted, #9a8870)", marginBottom: 10 }}>NOVA — Nível de processamento</div>
|
|
<RadioGroup
|
|
value={filters.novaGroups?.[0]?.toString() || ""}
|
|
onValueChange={(value) => updateFilter("novaGroups", value ? [parseInt(value) as 1 | 2 | 3 | 4] : [])}
|
|
className="space-y-2"
|
|
>
|
|
{NOVA_GROUPS.map((group) => (
|
|
<div key={group.group} style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
<RadioGroupItem value={group.group.toString()} id={`nova-${group.group}`} />
|
|
<label htmlFor={`nova-${group.group}`} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer", flex: 1 }}>
|
|
<Badge className={`${group.color} text-white text-xs`}>{group.label}</Badge>
|
|
<span style={{ color: "var(--fg-secondary, #5e4e3c)", fontSize: 11 }}>{group.description}</span>
|
|
</label>
|
|
</div>
|
|
))}
|
|
</RadioGroup>
|
|
</div>
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
|
|
{/* Categorias */}
|
|
<AccordionItem value="categories" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
|
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<LayoutGrid style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
|
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Categorias</span>
|
|
</div>
|
|
</AccordionTrigger>
|
|
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
|
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 10 }}>
|
|
<Input
|
|
placeholder="Buscar categorias…"
|
|
value={searchCategories}
|
|
onChange={(e) => setSearchCategories(e.target.value)}
|
|
style={{ height: 34, fontSize: 13, borderColor: "var(--border-subtle, #e8e0d0)", background: "var(--bg-base, #f9f6f0)" }}
|
|
/>
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 192, overflowY: "auto" }}>
|
|
{POPULAR_CATEGORIES_WITH_ICONS.filter(
|
|
(cat) =>
|
|
cat.label.toLowerCase().includes(searchCategories.toLowerCase()) ||
|
|
cat.value.toLowerCase().includes(searchCategories.toLowerCase())
|
|
).map((category) => {
|
|
const IconComponent = LucideIcons[category.icon as keyof typeof LucideIcons] as React.ComponentType<{ style?: React.CSSProperties }> | undefined;
|
|
return (
|
|
<div key={category.value} style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
<Checkbox
|
|
id={`category-${category.value}`}
|
|
checked={filters.categories?.includes(category.value) || false}
|
|
onCheckedChange={(checked) => updateArrayFilter("categories", category.value, checked as boolean)}
|
|
/>
|
|
<label htmlFor={`category-${category.value}`} style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 13, cursor: "pointer", color: "var(--fg-secondary, #5e4e3c)" }}>
|
|
{IconComponent && <IconComponent style={{ width: 13, height: 13, color: "var(--fg-muted, #9a8870)" } as React.CSSProperties} />}
|
|
{category.label}
|
|
</label>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
|
|
{/* Marcas */}
|
|
<AccordionItem value="brands" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
|
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<Tag style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
|
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Marcas</span>
|
|
</div>
|
|
</AccordionTrigger>
|
|
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
|
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 8 }}>
|
|
<Input
|
|
placeholder="Nome da marca…"
|
|
value={searchBrands}
|
|
onChange={(e) => setSearchBrands(e.target.value)}
|
|
style={{ height: 34, fontSize: 13, borderColor: "var(--border-subtle, #e8e0d0)", background: "var(--bg-base, #f9f6f0)" }}
|
|
/>
|
|
<div style={{ fontSize: 11, color: "var(--fg-muted, #9a8870)", letterSpacing: "0.02em" }}>Digite para filtrar por marca</div>
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
|
|
{/* Alérgenos */}
|
|
<AccordionItem value="allergens" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
|
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<AlertTriangle style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
|
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Alérgenos</span>
|
|
</div>
|
|
</AccordionTrigger>
|
|
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
|
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 10 }}>
|
|
<Input
|
|
placeholder="Buscar alérgenos…"
|
|
value={searchAllergens}
|
|
onChange={(e) => setSearchAllergens(e.target.value)}
|
|
style={{ height: 34, fontSize: 13, borderColor: "var(--border-subtle, #e8e0d0)", background: "var(--bg-base, #f9f6f0)" }}
|
|
/>
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 192, overflowY: "auto" }}>
|
|
{COMMON_ALLERGENS.filter((allergen) =>
|
|
allergen.toLowerCase().includes(searchAllergens.toLowerCase())
|
|
).map((allergen) => (
|
|
<div key={allergen} style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
<Checkbox
|
|
id={`allergen-${allergen}`}
|
|
checked={filters.allergens?.includes(allergen) || false}
|
|
onCheckedChange={(checked) => updateArrayFilter("allergens", allergen, checked as boolean)}
|
|
/>
|
|
<label htmlFor={`allergen-${allergen}`} style={{ fontSize: 13, cursor: "pointer", textTransform: "capitalize", color: "var(--fg-secondary, #5e4e3c)" }}>
|
|
{allergen}
|
|
</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
|
|
{/* Aditivos */}
|
|
<AccordionItem value="additives" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
|
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<Beaker style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
|
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Aditivos</span>
|
|
</div>
|
|
</AccordionTrigger>
|
|
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
|
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 8 }}>
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
<label htmlFor="no-additives" style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-secondary, #5e4e3c)", cursor: "pointer" }}>
|
|
Sem aditivos
|
|
</label>
|
|
<Switch
|
|
id="no-additives"
|
|
checked={filters.noAdditives || false}
|
|
onCheckedChange={(checked) => updateFilter("noAdditives", checked)}
|
|
/>
|
|
</div>
|
|
<div style={{ fontSize: 11, color: "var(--fg-muted, #9a8870)" }}>
|
|
Mostrar apenas produtos sem aditivos
|
|
</div>
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
|
|
{/* Valores Nutricionais */}
|
|
<AccordionItem value="nutrition-values" style={{ border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, overflow: "hidden" }}>
|
|
<AccordionTrigger className="px-4 py-3 hover:no-underline">
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<BarChart3 style={{ width: 14, height: 14, color: "var(--accent, oklch(52% 0.11 155))" }} strokeWidth={1.75} />
|
|
<span style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary, #1e1810)" }}>Valores Nutricionais</span>
|
|
</div>
|
|
</AccordionTrigger>
|
|
<AccordionContent className="px-4 pb-4" style={{ borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
|
<div style={{ paddingTop: 12, display: "flex", flexDirection: "column", gap: 18 }}>
|
|
{[
|
|
{ key: "energy", label: "Energia", unit: "kcal", max: 500, step: 10 },
|
|
{ key: "sugars", label: "Açúcares", unit: "g/100g", max: 50, step: 1 },
|
|
{ key: "sodium", label: "Sódio", unit: "g/100g", max: 3, step: 0.1 },
|
|
].map(({ key, label, unit, max, step }) => (
|
|
<div key={key} style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
<label style={{ fontSize: 12, fontWeight: 500, color: "var(--fg-secondary, #5e4e3c)" }}>{label} <span style={{ color: "var(--fg-muted)", fontWeight: 400 }}>({unit})</span></label>
|
|
<div style={{ paddingLeft: 4, paddingRight: 4 }}>
|
|
<Slider
|
|
value={[
|
|
(filters.nutritionRanges as Record<string, { min?: number; max?: number }>)?.[key]?.min || 0,
|
|
(filters.nutritionRanges as Record<string, { min?: number; max?: number }>)?.[key]?.max || max,
|
|
]}
|
|
onValueChange={([min, maxVal]) => {
|
|
updateNutritionRange(key, "min", min);
|
|
updateNutritionRange(key, "max", maxVal);
|
|
}}
|
|
max={max}
|
|
step={step}
|
|
className="w-full"
|
|
/>
|
|
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 4 }}>
|
|
<span style={{ fontSize: 11, color: "var(--fg-muted, #9a8870)" }}>
|
|
{(filters.nutritionRanges as Record<string, { min?: number; max?: number }>)?.[key]?.min || 0}{key === "energy" ? " kcal" : "g"}
|
|
</span>
|
|
<span style={{ fontSize: 11, color: "var(--fg-muted, #9a8870)" }}>
|
|
{(filters.nutritionRanges as Record<string, { min?: number; max?: number }>)?.[key]?.max || max}{key === "energy" ? " kcal" : "g"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
|
|
</Accordion>
|
|
|
|
{/* ── Footer action ─────────────────────────────────────────────────── */}
|
|
<div style={{ paddingTop: 16, borderTop: "1px solid var(--border-subtle, #e8e0d0)" }}>
|
|
<button
|
|
onClick={onClearFilters}
|
|
disabled={!hasActiveFilters}
|
|
style={{ width: "100%", padding: "10px 0", border: "1px solid var(--border-subtle, #e8e0d0)", borderRadius: 10, background: "transparent", fontSize: 13, color: hasActiveFilters ? "var(--fg-secondary, #5e4e3c)" : "var(--fg-muted, #9a8870)", fontFamily: "inherit", cursor: hasActiveFilters ? "pointer" : "not-allowed", transition: "all 0.15s" }}
|
|
>
|
|
Limpar todos os filtros
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|