feat: filtr jednostek multisplit, ostatnio oglądane, sklep pompy ciepła, czyszczenie komentarzy, checkbox zgody

- Filtr 'Ilość jednostek wewn.' w sklepie (1–9 jednostek, checkboxy, liczniki)
- Komponent RecentlyViewedProducts + Zustand store (recently-viewed-store.ts)
- Pompy ciepła dodane do sklepu i nawigacji (Header + MobileMenu)
- Logotypy marek zaktualizowane (Daikin, Panasonic + odświeżone istniejące)
- Checkbox zgody RODO w /zamowienie powiększony (w-5 h-5)
- Usunięcie ~65 zbędnych komentarzy WHAT z 12 plików (build: 0 errors)
- Plik analyze_products.ts (pomocniczy skrypt diagnostyczny)
This commit is contained in:
MichalC
2026-08-02 20:31:55 +02:00
parent 092cff1899
commit c9e4283789
37 changed files with 331 additions and 123 deletions
+77 -22
View File
@@ -81,8 +81,8 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
const urlBrand = searchParams.get('brand') || '';
const urlCategory = searchParams.get('category') || 'all';
const [category, setCategory] = useState<'all' | 'scienne' | 'multisplit' | 'podlogowe' | 'jednostka-scienna'>(
urlCategory as 'all' | 'scienne' | 'multisplit' | 'podlogowe' | 'jednostka-scienna'
const [category, setCategory] = useState<'all' | 'scienne' | 'multisplit' | 'podlogowe' | 'jednostka-scienna' | 'pompy-ciepla'>(
urlCategory as 'all' | 'scienne' | 'multisplit' | 'podlogowe' | 'jednostka-scienna' | 'pompy-ciepla'
);
const [selectedBrands, setSelectedBrands] = useState<string[]>(urlBrand ? [urlBrand] : []);
const [searchQuery, setSearchQuery] = useState(urlSearch);
@@ -90,6 +90,7 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
const [priceMax, setPriceMax] = useState('');
const [selectedRooms, setSelectedRooms] = useState<string[]>([]);
const [selectedColors, setSelectedColors] = useState<string[]>([]);
const [selectedUnitCounts, setSelectedUnitCounts] = useState<number[]>([]);
const [halasMin, setHalasMin] = useState(15);
const [halasMax, setHalasMax] = useState(50);
const [page, setPage] = useState(1);
@@ -100,7 +101,7 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
setSearchQuery(urlSearch);
setSelectedBrands(urlBrand ? [urlBrand] : []);
if (urlCategory !== 'all') {
setCategory(urlCategory as 'scienne' | 'multisplit' | 'podlogowe' | 'jednostka-scienna');
setCategory(urlCategory as 'scienne' | 'multisplit' | 'podlogowe' | 'jednostka-scienna' | 'pompy-ciepla');
} else {
setCategory('all');
}
@@ -124,6 +125,11 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
setPage(1);
};
const toggleUnitCount = (uc: number) => {
setSelectedUnitCounts(prev => prev.includes(uc) ? prev.filter(x => x !== uc) : [...prev, uc]);
setPage(1);
};
const filtered = useMemo(() => {
return products.filter(p => {
// Wyszukiwanie tekstowe
@@ -134,13 +140,10 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
}
}
// Kategoria
if (category !== 'all' && p.category !== category) return false;
// Marka
if (selectedBrands.length > 0 && !selectedBrands.includes(p.brand)) return false;
// Cena
const pMin = priceMin ? parseInt(priceMin) : null;
const pMax = priceMax ? parseInt(priceMax) : null;
if (p.price !== null) {
@@ -172,9 +175,17 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
if (!selectedColors.includes(group)) return false;
}
// Liczba jednostek wewn. (multisplit)
if (selectedUnitCounts.length > 0) {
const unitsStr = p.specs['Liczba jednostek wewn.'];
if (!unitsStr) return false;
const units = parseFloat(unitsStr);
if (!selectedUnitCounts.includes(units)) return false;
}
return true;
});
}, [products, category, selectedBrands, priceMin, priceMax, selectedRooms, halasMin, halasMax, selectedColors, searchQuery]);
}, [products, category, selectedBrands, priceMin, priceMax, selectedRooms, halasMin, halasMax, selectedColors, selectedUnitCounts, searchQuery]);
const totalPages = Math.ceil(filtered.length / PER_PAGE);
const paginated = filtered.slice((page - 1) * PER_PAGE, page * PER_PAGE);
@@ -205,6 +216,22 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
return counts;
}, [products, category]);
// Liczniki per liczba jednostek wewn.
const unitCountsAgg = useMemo(() => {
const counts: Record<number, number> = {};
products.forEach(p => {
if (category !== 'all' && p.category !== category) return;
const unitsStr = p.specs['Liczba jednostek wewn.'];
if (unitsStr) {
const units = parseFloat(unitsStr);
if (!isNaN(units)) {
counts[units] = (counts[units] || 0) + 1;
}
}
});
return counts;
}, [products, category]);
const resetAll = () => {
setCategory('all');
setSelectedBrands([]);
@@ -213,22 +240,23 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
setPriceMax('');
setSelectedRooms([]);
setSelectedColors([]);
setSelectedUnitCounts([]);
setHalasMin(15);
setHalasMax(50);
setPage(1);
};
const hasFilters = category !== 'all' || selectedBrands.length > 0 || searchQuery !== '' || priceMin || priceMax || selectedRooms.length > 0 || selectedColors.length > 0 || halasMin > 15 || halasMax < 50;
const hasFilters = category !== 'all' || selectedBrands.length > 0 || searchQuery !== '' || priceMin || priceMax || selectedRooms.length > 0 || selectedColors.length > 0 || selectedUnitCounts.length > 0 || halasMin > 15 || halasMax < 50;
const sidebarContent = (
<aside className="w-full">
{/* Klimatyzatory - typ */}
<Accordion title="Klimatyzatory">
<ul className="flex flex-col gap-2">
{[
{ val: 'all', label: 'Wszystkie' },
{ val: 'scienne', label: 'Ścienne split' },
{ val: 'multisplit', label: 'Multisplit' },
{ val: 'pompy-ciepla', label: 'Pompy ciepła' },
].map(opt => (
<li key={opt.val}>
<button
@@ -250,7 +278,6 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
</ul>
</Accordion>
{/* Marka */}
<Accordion title="Marka">
<ul className="flex flex-col gap-2">
{brands.map(b => (
@@ -285,7 +312,46 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
</ul>
</Accordion>
{/* Cena */}
{/* Ilość jednostek wewnętrznych */}
{(category === 'all' || category === 'multisplit') && (
<Accordion title="Ilość jednostek wewn.">
<ul className="flex flex-col gap-2">
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map(uc => {
if (unitCountsAgg[uc] === undefined && !selectedUnitCounts.includes(uc)) return null;
return (
<li key={uc}>
<label className="flex items-center gap-2.5 cursor-pointer group">
<input
type="checkbox"
checked={selectedUnitCounts.includes(uc)}
onChange={() => toggleUnitCount(uc)}
className="sr-only"
/>
<span className={`size-4 rounded border-2 shrink-0 flex items-center justify-center transition-colors ${
selectedUnitCounts.includes(uc)
? 'bg-accent border-accent'
: 'bg-white border-slate-300 group-hover:border-accent'
}`}>
{selectedUnitCounts.includes(uc) && (
<svg viewBox="0 0 10 8" className="size-2.5" fill="none">
<path d="M1 4L3.5 6.5L9 1" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</span>
<span className={`text-sm transition-colors ${selectedUnitCounts.includes(uc) ? 'text-primary font-medium' : 'text-text-muted group-hover:text-primary'}`}>
{uc} {uc === 1 ? 'jednostka' : uc >= 2 && uc <= 4 ? 'jednostki' : 'jednostek'}
</span>
<span className="ml-auto text-xs text-text-muted">
{unitCountsAgg[uc] || 0}
</span>
</label>
</li>
);
})}
</ul>
</Accordion>
)}
<Accordion title="Cena (zł)">
<div className="flex items-center gap-2">
<input
@@ -345,7 +411,6 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
{/* Głośność */}
<Accordion title={`Głośność wewnętrzna (dB) — ${halasMin}–${halasMax} dB`}>
<div className="flex flex-col gap-4 px-1">
{/* od */}
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<span className="text-xs text-text-muted">od</span>
@@ -359,7 +424,6 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
className="range range-xs range-accent w-full"
/>
</div>
{/* do */}
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<span className="text-xs text-text-muted">do</span>
@@ -376,7 +440,6 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
</div>
</Accordion>
{/* Kolor/Wykończenie */}
<Accordion title="Kolor dominujący">
<ul className="flex flex-col gap-2">
{COLOR_GROUPS.map(c => (
@@ -408,7 +471,6 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
</ul>
</Accordion>
{/* Potrzebujesz pomocy */}
<div className="rounded-xl bg-slate-50 border border-border p-4 text-center mt-2">
<p className="text-sm font-bold text-primary font-heading mb-3">Potrzebujesz pomocy?</p>
<a
@@ -432,7 +494,6 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
<section className="py-8 md:py-12 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Mobile: przycisk filtrów */}
<div className="lg:hidden mb-4 flex items-center justify-between">
<button
onClick={() => setMobileSidebarOpen(true)}
@@ -444,7 +505,6 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
<span className="text-sm text-text-muted">{filtered.length} produktów</span>
</div>
{/* Mobile sidebar overlay */}
{mobileSidebarOpen && (
<div className="fixed inset-0 z-50 lg:hidden">
<div className="absolute inset-0 bg-black/50" onClick={() => setMobileSidebarOpen(false)} />
@@ -461,7 +521,6 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
)}
<div className="flex gap-8">
{/* Desktop sidebar */}
<div className="hidden lg:block w-64 shrink-0">
<div className="sticky top-24 max-h-[calc(100vh-7rem)] overflow-y-auto pr-2">
{hasFilters && (
@@ -476,9 +535,7 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
</div>
</div>
{/* Produkty */}
<div className="flex-1 min-w-0">
{/* Nagłówek z licznikiem */}
<div className="flex items-center justify-between mb-6">
<span className="text-sm text-text-muted hidden lg:block">{filtered.length} produktów</span>
{hasFilters && (
@@ -488,7 +545,6 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
)}
</div>
{/* Grid */}
{paginated.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-5">
{paginated.map(product => (
@@ -504,7 +560,6 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
</div>
)}
{/* Paginacja */}
{totalPages > 1 && (
<div className="flex justify-center gap-2 mt-12">
<button