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:
@@ -33,7 +33,6 @@ export function CartItems() {
|
||||
|
||||
return (
|
||||
<div key={item.id} className="bg-white p-6 rounded-2xl border border-border flex flex-col sm:flex-row gap-6 relative">
|
||||
{/* Usuń pozycję */}
|
||||
<button
|
||||
onClick={() => removeItem(item.id)}
|
||||
className="absolute top-4 right-4 p-2 text-text-muted hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||||
@@ -42,7 +41,6 @@ export function CartItems() {
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Obraz */}
|
||||
<div className="relative w-24 h-24 sm:w-32 sm:h-32 shrink-0 bg-slate-50 rounded-xl p-2 border border-border">
|
||||
<Image
|
||||
src={item.image}
|
||||
@@ -52,7 +50,6 @@ export function CartItems() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Treść */}
|
||||
<div className="flex flex-col flex-grow justify-between">
|
||||
<div>
|
||||
<div className="text-xs font-bold text-text-muted uppercase tracking-wider mb-1">
|
||||
@@ -65,9 +62,7 @@ export function CartItems() {
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Opcja Montażu i Cena jednostkowa */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-end gap-4 mt-4">
|
||||
{/* Ilość i Cena */}
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center border border-border rounded-lg bg-slate-50">
|
||||
<button
|
||||
|
||||
@@ -20,7 +20,6 @@ export function CartSummary() {
|
||||
<div className="bg-slate-50 p-6 md:p-8 rounded-2xl border border-border sticky top-24">
|
||||
<h3 className="text-xl font-bold text-primary font-heading mb-6">Podsumowanie zamówienia</h3>
|
||||
|
||||
{/* Kod rabatowy */}
|
||||
<div className="mb-8">
|
||||
<label htmlFor="promo" className="text-sm font-semibold text-text-muted mb-2 block">Posiadasz kod rabatowy?</label>
|
||||
<div className="flex gap-2">
|
||||
@@ -39,7 +38,6 @@ export function CartSummary() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kwoty */}
|
||||
<div className="space-y-4 mb-6 border-b border-border/50 pb-6">
|
||||
<div className="flex justify-between text-sm text-text-muted">
|
||||
<span>Suma netto</span>
|
||||
@@ -51,13 +49,11 @@ export function CartSummary() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div className="flex justify-between items-end mb-8">
|
||||
<span className="text-lg font-bold text-primary">Do zapłaty brutto</span>
|
||||
<span className="text-3xl font-extrabold text-primary font-heading text-right">{formatCurrency(total)}</span>
|
||||
</div>
|
||||
|
||||
{/* Przycisk */}
|
||||
<Link href="/zamowienie" className="block w-full btn btn-primary font-bold group cursor-pointer text-white">
|
||||
|
||||
Przejdź do zamówienia
|
||||
@@ -65,7 +61,6 @@ export function CartSummary() {
|
||||
|
||||
</Link>
|
||||
|
||||
{/* Zaufanie */}
|
||||
<div className="mt-6 flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2 text-xs text-text-muted font-medium justify-center">
|
||||
<ShieldCheck className="w-4 h-4 text-green-600" />
|
||||
|
||||
@@ -66,7 +66,6 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
|
||||
return (
|
||||
<div className="group relative bg-white rounded-xl border border-border hover:border-accent hover:shadow-md transition-all duration-200 overflow-hidden flex flex-col">
|
||||
{/* Zdjęcie */}
|
||||
<Link href={`/sklep/${product.slug}`} className="block relative aspect-[4/3] bg-slate-50 overflow-hidden">
|
||||
<Image
|
||||
src={product.image}
|
||||
@@ -77,7 +76,6 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
unoptimized={product.image.startsWith('http')}
|
||||
/>
|
||||
|
||||
{/* Brand Logo or Name */}
|
||||
<div className="absolute top-3 left-3 z-10 flex flex-col gap-1.5">
|
||||
{brandLogos[product.brand] ? (
|
||||
<div className="bg-white rounded px-1.5 py-1 inline-block shadow-sm">
|
||||
@@ -97,7 +95,6 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Badge multisplit */}
|
||||
{product.category === 'multisplit' && (
|
||||
<span className="absolute top-2 right-2 px-2 py-1 bg-accent/10 rounded-md text-xs font-semibold text-accent border border-accent/20">
|
||||
Multisplit
|
||||
@@ -119,7 +116,6 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
{/* Treść */}
|
||||
<div className="p-4 flex flex-col flex-1 gap-2">
|
||||
<Link href={`/sklep/${product.slug}`}>
|
||||
<h3 className="text-sm font-semibold text-primary font-heading leading-snug line-clamp-2 hover:text-accent transition-colors">
|
||||
@@ -127,7 +123,6 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
</h3>
|
||||
</Link>
|
||||
|
||||
{/* Kluczowe specs */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{product.specs['Moc chłodnicza'] && (
|
||||
<span className="px-2 py-0.5 bg-slate-50 rounded text-xs text-text-muted border border-border">
|
||||
@@ -149,7 +144,6 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
{/* Cena + CTA */}
|
||||
<div className="mt-auto pt-2 flex items-end justify-between gap-2">
|
||||
{product.category === 'multisplit' ? (
|
||||
// Multisplit — zawsze formularz zapytania
|
||||
<>
|
||||
<div className="flex items-center gap-1 text-accent">
|
||||
<Phone className="size-4" />
|
||||
@@ -165,7 +159,6 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
</Link>
|
||||
</>
|
||||
) : product.price !== null ? (
|
||||
// Split z ceną — koszyk
|
||||
<>
|
||||
<div>
|
||||
<p className="text-lg font-bold text-primary font-heading leading-none">
|
||||
@@ -191,7 +184,6 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// Split bez ceny — zapytaj
|
||||
<>
|
||||
<div className="flex items-center gap-1 text-accent">
|
||||
<Phone className="size-3.5" />
|
||||
|
||||
@@ -12,14 +12,12 @@ interface ProductGalleryProps {
|
||||
export function ProductGallery({ image, name, images }: ProductGalleryProps) {
|
||||
const [mainImage, setMainImage] = useState(image || '/images/hero/shop-section-image.webp');
|
||||
|
||||
// Tworzymy tablicę miniatur, używamy placeholdera jako drugiej by pokazać funkcjonalność
|
||||
const gallery = images && images.length > 0
|
||||
? images
|
||||
: [image, '/images/hero/shop-section-image.webp'];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Główne zdjęcie */}
|
||||
<div className="relative aspect-square w-full bg-slate-50 rounded-2xl border border-border overflow-hidden flex items-center justify-center p-8">
|
||||
<Image
|
||||
src={mainImage}
|
||||
@@ -31,7 +29,6 @@ export function ProductGallery({ image, name, images }: ProductGalleryProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Miniatury */}
|
||||
{gallery.length > 1 && (
|
||||
<div className="flex gap-4 overflow-x-auto pb-2">
|
||||
{gallery.map((img, idx) => (
|
||||
|
||||
@@ -11,6 +11,9 @@ import { Product } from '@/content/products';
|
||||
import { useCartStore } from '@/lib/cart-store';
|
||||
import { useCompareStore } from '@/lib/compare-store';
|
||||
import { getProductVariants } from '@/content/product-variants';
|
||||
import { useRecentlyViewedStore } from '@/lib/recently-viewed-store';
|
||||
import { RecentlyViewedProducts } from './RecentlyViewedProducts';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const brandLogos: Record<string, string> = {
|
||||
'Gree': '/brands/gree.webp',
|
||||
@@ -34,7 +37,6 @@ interface Props {
|
||||
companyPhoneRaw: string;
|
||||
}
|
||||
|
||||
// Szacowany SEER na podstawie klasy energetycznej
|
||||
function estimateSEER(klasa: string): number {
|
||||
if (!klasa) return 5.0;
|
||||
const k = klasa.toUpperCase();
|
||||
@@ -45,7 +47,6 @@ function estimateSEER(klasa: string): number {
|
||||
return 5.0;
|
||||
}
|
||||
|
||||
// Parsuj moc — bierz wartość nominalną (środkową lub pierwszą)
|
||||
function parseMoc(val: string): number {
|
||||
if (!val) return 0;
|
||||
const nums = val.replace(',', '.').match(/[\d.]+/g);
|
||||
@@ -81,6 +82,11 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
const { addItem: addCompare, removeItem: removeCompare, isAdded } = useCompareStore();
|
||||
const inCompare = isAdded(currentProduct.slug);
|
||||
|
||||
const addRecentlyViewed = useRecentlyViewedStore((s) => s.addItem);
|
||||
useEffect(() => {
|
||||
addRecentlyViewed(currentProduct);
|
||||
}, [currentProduct, addRecentlyViewed]);
|
||||
|
||||
const handleAddToCart = () => {
|
||||
if (!currentProduct.price) return;
|
||||
addToCart({
|
||||
@@ -99,14 +105,12 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
else addCompare({ slug: currentProduct.slug, name: currentProduct.name, brand: currentProduct.brand, price: currentProduct.price, image: currentProduct.image, specs: currentProduct.specs });
|
||||
};
|
||||
|
||||
// Kalkulator
|
||||
const mocChlodnicza = parseMoc(currentProduct.specs['Moc chłodnicza'] || '');
|
||||
const pobórMocy = parseMoc(currentProduct.specs['Pobór mocy chłodzenie (kW)'] || '') || (mocChlodnicza / estimateSEER(currentProduct.specs['Klasa energetyczna'] || ''));
|
||||
const cenaKwh = 0.75;
|
||||
const kosztGodzina = pobórMocy * cenaKwh;
|
||||
const kosztMiesiac = kosztGodzina * godziny * 30;
|
||||
|
||||
// Kluczowe parametry do ikon
|
||||
const params = [
|
||||
currentProduct.specs['Klasa energetyczna'] && { icon: Star, label: 'Klasa energetyczna', value: currentProduct.specs['Klasa energetyczna'], color: 'text-green-600 bg-green-50' },
|
||||
currentProduct.specs['Moc chłodnicza'] && { icon: Thermometer, label: 'Moc chłodnicza', value: `${currentProduct.specs['Moc chłodnicza']} kW`, color: 'text-blue-600 bg-blue-50' },
|
||||
@@ -120,7 +124,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
<main className="bg-slate-50 min-h-screen pb-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-1.5 text-xs text-text-muted mb-6">
|
||||
<Link href="/" className="hover:text-accent transition-colors">Strona główna</Link>
|
||||
<span>/</span>
|
||||
@@ -133,7 +136,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 xl:gap-12">
|
||||
|
||||
{/* LEWA: Zdjęcie */}
|
||||
<div>
|
||||
<div className="relative aspect-[4/3] lg:aspect-auto lg:h-[420px] lg:max-h-[420px] bg-white rounded-2xl border border-border overflow-hidden">
|
||||
<Image
|
||||
@@ -144,7 +146,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
priority
|
||||
unoptimized={currentProduct.image.startsWith('http')}
|
||||
/>
|
||||
{/* Badges */}
|
||||
{currentProduct.specs['Klasa energetyczna'] && (
|
||||
<div className="absolute top-4 left-4 px-2.5 py-1 bg-green-500 text-white text-xs font-bold rounded-lg shadow">
|
||||
{currentProduct.specs['Klasa energetyczna'].split('/')[0]}
|
||||
@@ -158,9 +159,7 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PRAWA: Info */}
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Nagłówek */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-3 flex-wrap">
|
||||
{brandLogos[currentProduct.brand] ? (
|
||||
@@ -195,7 +194,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
<p className="text-xs text-text-muted mt-1.5 mb-4">SKU: {currentProduct.sku}</p>
|
||||
</div>
|
||||
|
||||
{/* Warianty mocy */}
|
||||
{variants && variants.moce.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<p className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
|
||||
@@ -235,7 +233,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warianty wykończenia */}
|
||||
{variants && variants.wykończenia.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<p className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
|
||||
@@ -265,7 +262,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ikony parametrów */}
|
||||
{params.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{params.map((p, i) => {
|
||||
@@ -283,7 +279,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cena i CTA */}
|
||||
<div className="bg-white rounded-2xl border border-border p-4">
|
||||
{currentProduct.price !== null ? (
|
||||
<>
|
||||
@@ -347,7 +342,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Profesjonalna porada */}
|
||||
<a
|
||||
href={`tel:${companyPhoneRaw}`}
|
||||
className="flex items-center justify-between px-5 py-2.5 bg-primary rounded-xl text-white hover:bg-primary/90 transition-colors"
|
||||
@@ -358,7 +352,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{/* Kalkulator kosztów energii */}
|
||||
{mocChlodnicza > 0 && (
|
||||
<div className="bg-white rounded-2xl border border-border p-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
@@ -408,7 +401,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABS */}
|
||||
<div className="mt-10">
|
||||
<div className="flex gap-1 border-b border-border mb-6 overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||
{TABS.map(tab => (
|
||||
@@ -426,7 +418,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab: Opis */}
|
||||
{activeTab === 'Opis' && (
|
||||
<div className="max-w-3xl">
|
||||
{currentProduct.shortDescription ? (
|
||||
@@ -437,7 +428,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab: Specyfikacja */}
|
||||
{activeTab === 'Specyfikacja' && (
|
||||
<div className="max-w-2xl">
|
||||
{Object.keys(currentProduct.specs).length > 0 ? (
|
||||
@@ -459,7 +449,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab: Montaż */}
|
||||
{activeTab === 'Montaż' && (
|
||||
<div className="max-w-2xl space-y-4">
|
||||
<div className="bg-white rounded-xl border border-border p-5">
|
||||
@@ -483,7 +472,6 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab: Opinie */}
|
||||
{activeTab === 'Opinie' && (
|
||||
<div className="max-w-3xl space-y-8">
|
||||
<div className="bg-slate-50 p-6 rounded-2xl border border-border">
|
||||
@@ -512,7 +500,7 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<RecentlyViewedProducts currentSlug={currentProduct.slug} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecentlyViewedStore } from '@/lib/recently-viewed-store';
|
||||
import { ProductCard } from './ProductCard';
|
||||
|
||||
export function RecentlyViewedProducts({ currentSlug }: { currentSlug: string }) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const items = useRecentlyViewedStore((state) => state.items);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setMounted(true), 0);
|
||||
return () => clearTimeout(t);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const displayItems = items.filter(item => item.slug !== currentSlug).slice(0, 4);
|
||||
|
||||
if (displayItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-16 border-t border-border pt-12">
|
||||
<h2 className="text-2xl font-bold text-primary font-heading mb-6">
|
||||
Ostatnio oglądane przez Ciebie
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{displayItems.map(product => (
|
||||
<ProductCard key={product.slug} product={product} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user