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
+10
View File
@@ -0,0 +1,10 @@
import { products } from './content/products';
const values = new Set<number>();
products.filter(p => p.category === 'multisplit').forEach(p => {
if (p.specs && p.specs['Liczba jednostek wewn.']) {
values.add(parseFloat(p.specs['Liczba jednostek wewn.']));
}
});
const sorted = Array.from(values).sort((a, b) => a - b);
console.log('Unique unit counts:', sorted);
+2 -2
View File
@@ -250,11 +250,11 @@ export default function CennikMontazuPage() {
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="lg">
<Button variant="primary" size="md">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
<Link href="/kontakt" className={buttonClasses({ variant: 'outline', size: 'lg' })}>
<Link href="/kontakt" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Napisz do nas <ArrowRight className="size-5" />
+13 -1
View File
@@ -209,4 +209,16 @@
color: #ffffff !important;
border-color: var(--accent) !important;
}
}
}
@layer utilities {
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(calc(-50% - 0.5rem)); }
}
.animate-marquee {
animation: marquee 25s linear infinite;
}
.animate-marquee:hover {
animation-play-state: paused;
}
}
+2 -2
View File
@@ -276,11 +276,11 @@ export default function KlimatyzacjaDoBiuraPage() {
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="lg">
<Button variant="primary" size="md">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
<Link href="/kontakt" className={buttonClasses({ variant: 'outline', size: 'lg' })}>
<Link href="/kontakt" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Napisz do nas <ArrowRight className="size-5" />
+2 -2
View File
@@ -245,11 +245,11 @@ export default function KlimatyzacjaDoDomuPage() {
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="lg">
<Button variant="primary" size="md">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
<Link href="/kalkulator" className={buttonClasses({ variant: 'outline', size: 'lg' })}>
<Link href="/kalkulator" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Kalkulator doboru <ArrowRight className="size-5" />
+1 -1
View File
@@ -253,7 +253,7 @@ export default function MontazKlimatyzacjiPage() {
Zadzwoń po bezpłatną wycenę — odpowiadamy szybko i przyjeżdżamy w całym Dolnym Śląsku.
</p>
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="lg" className="mx-auto">
<Button variant="primary" size="md" className="mx-auto">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
+2 -2
View File
@@ -196,11 +196,11 @@ export default function SerwisDaikinPage() {
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="lg">
<Button variant="primary" size="md">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
<Link href="/zgloszenie-awarii" className={buttonClasses({ variant: 'outline', size: 'lg' })}>
<Link href="/zgloszenie-awarii" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Zgłoś online <ArrowRight className="size-5" />
+33 -13
View File
@@ -83,7 +83,18 @@ const USLUGI = [
},
];
const MARKI = ['Daikin', 'Mitsubishi Electric', 'LG', 'Samsung', 'Gree', 'Rotenso', 'Haier', 'Aux', 'Fuji Electric', 'Toshiba', 'Fujitsu', 'Bosch'];
const BRANDS_WITH_LOGOS = [
{ name: 'Aux', src: '/brands/aux-brand.webp' },
{ name: 'CHV', src: '/brands/chv.webp' },
{ name: 'Fuji Electric', src: '/brands/fuji.webp' },
{ name: 'Gree', src: '/brands/gree.webp' },
{ name: 'Haier', src: '/brands/haier.webp' },
{ name: 'LG', src: '/brands/lg.webp' },
{ name: 'Mitsubishi Electric', src: '/brands/mitsubishi.webp' },
{ name: 'Rotenso', src: '/brands/rotenso.webp' },
{ name: 'Samsung', src: '/brands/samsung.webp' },
{ name: 'Toshiba', src: '/brands/toshiba.webp' },
];
export default function SerwisKlimatyzacjiPage() {
const jsonLd = {
@@ -189,16 +200,27 @@ export default function SerwisKlimatyzacjiPage() {
</section>
{/* Marki */}
<section className="py-16 bg-slate-50">
<section className="py-16 bg-slate-50 overflow-hidden">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-extrabold text-primary font-heading mb-6">Serwisujemy wszystkie popularne marki</h2>
<div className="flex flex-wrap gap-2 mb-4">
{MARKI.map(m => (
<span key={m} className="px-4 py-2 bg-white border border-border rounded-xl text-sm font-semibold text-primary">
{m}
</span>
))}
<div className="relative flex overflow-hidden w-full mb-4">
<div className="flex w-max animate-marquee gap-4 whitespace-nowrap items-center">
{/* Pierwszy zestaw */}
{BRANDS_WITH_LOGOS.map(m => (
<div key={m.name} className="flex-shrink-0 px-6 py-4 bg-white border border-border rounded-xl flex items-center justify-center w-40 h-20 hover:shadow-md transition-shadow">
<Image src={m.src} alt={m.name} width={120} height={60} className="object-contain w-full h-full" />
</div>
))}
{/* Drugi zestaw (klon) do płynnego przejścia */}
{BRANDS_WITH_LOGOS.map(m => (
<div key={`dup-${m.name}`} className="flex-shrink-0 px-6 py-4 bg-white border border-border rounded-xl flex items-center justify-center w-40 h-20 hover:shadow-md transition-shadow">
<Image src={m.src} alt={m.name} width={120} height={60} className="object-contain w-full h-full" />
</div>
))}
</div>
</div>
<p className="text-xs text-text-muted">Nie widzisz swojej marki? Zadzwoń — prawdopodobnie też ją obsługujemy.</p>
</div>
</section>
@@ -234,15 +256,13 @@ export default function SerwisKlimatyzacjiPage() {
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="lg">
<Button variant="primary" size="md">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
<Link href="/zgloszenie-awarii" className={buttonClasses({ variant: 'outline', size: 'lg' })}>
<Link href="/zgloszenie-awarii" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Zgłoś online <ArrowRight className="size-5" />
</Link>
</Link>
</div>
</div>
</section>
+1 -1
View File
@@ -191,7 +191,7 @@ export default function SerwisLGPage() {
Zadzwoń lub napisz — odpowiadamy szybko i przyjeżdżamy na Dolny Śląsk.
</p>
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="lg" className="mx-auto">
<Button variant="primary" size="md" className="mx-auto">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
+2 -2
View File
@@ -200,11 +200,11 @@ export default function SerwisMitsubishiPage() {
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="lg">
<Button variant="primary" size="md">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
<Link href="/zgloszenie-awarii" className={buttonClasses({ variant: 'outline', size: 'lg' })}>
<Link href="/zgloszenie-awarii" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Zgłoś online <ArrowRight className="size-5" />
+2 -2
View File
@@ -196,11 +196,11 @@ export default function SerwisSamsungPage() {
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="lg">
<Button variant="primary" size="md">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
<Link href="/zgloszenie-awarii" className={buttonClasses({ variant: 'outline', size: 'lg' })}>
<Link href="/zgloszenie-awarii" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Zgłoś online <ArrowRight className="size-5" />
+1 -1
View File
@@ -122,7 +122,7 @@ Wróć do sklepu
</div>
<label className="flex items-start gap-3 cursor-pointer">
<input type="checkbox" name="consent" required className="mt-0.5 accent-accent" />
<input type="checkbox" name="consent" required className="mt-0.5 w-5 h-5 accent-accent shrink-0 cursor-pointer" />
<span className="text-xs text-text-muted">Wyrażam zgodę na przetwarzanie danych osobowych w celu realizacji zamówienia zgodnie z Polityką Prywatności. *</span>
</label>
-7
View File
@@ -59,7 +59,6 @@ export default function CookieBanner() {
return (
<div className="fixed bottom-0 left-0 right-0 z-[100] p-0 sm:p-6">
<div className="max-w-4xl mx-auto bg-white sm:rounded-2xl shadow-[0_-4px_20px_rgba(0,0,0,0.1)] sm:shadow-2xl border-t sm:border border-border overflow-hidden pb-safe sm:pb-0">
{/* Główny pasek */}
<div className="p-4 sm:p-6">
<div className="flex items-start gap-3 mb-3 sm:mb-4">
<span className="hidden sm:block text-2xl shrink-0">🍪</span>
@@ -77,10 +76,8 @@ export default function CookieBanner() {
</div>
</div>
{/* Szczegóły */}
{showDetails && (
<div className="flex flex-col gap-3 mb-5 p-4 bg-slate-50 rounded-xl">
{/* Niezbędne */}
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-sm font-semibold text-primary">Niezbędne</p>
@@ -95,7 +92,6 @@ export default function CookieBanner() {
</div>
</div>
{/* Analityczne */}
<div className="flex items-start justify-between gap-4 border-t border-border pt-3">
<div>
<p className="text-sm font-semibold text-primary">Analityczne</p>
@@ -113,7 +109,6 @@ export default function CookieBanner() {
</button>
</div>
{/* Marketingowe */}
<div className="flex items-start justify-between gap-4 border-t border-border pt-3">
<div>
<p className="text-sm font-semibold text-primary">Marketingowe</p>
@@ -131,7 +126,6 @@ export default function CookieBanner() {
</button>
</div>
{/* Reklamowe */}
<div className="flex items-start justify-between gap-4 border-t border-border pt-3">
<div>
<p className="text-sm font-semibold text-primary">Reklamowe</p>
@@ -151,7 +145,6 @@ export default function CookieBanner() {
</div>
)}
{/* Przyciski */}
<div className="grid grid-cols-2 sm:flex sm:flex-row items-stretch sm:items-center gap-2">
<button
onClick={storeAcceptAll}
-9
View File
@@ -114,7 +114,6 @@ export function AwariaClient() {
const [consent, setConsent] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// Podpowiedź kodu błędu
const errorHint = brand && errorCode && ERROR_CODES[brand]?.[errorCode.toUpperCase()];
const handleSubmit = async (e: React.FormEvent) => {
@@ -180,7 +179,6 @@ export function AwariaClient() {
return (
<main className="bg-slate-50 min-h-screen py-12">
<div className="max-w-2xl mx-auto px-4 sm:px-6">
{/* Header */}
<div className="text-center mb-8">
<div className="size-14 bg-red-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Wrench className="size-7 text-red-600" />
@@ -191,7 +189,6 @@ export function AwariaClient() {
<p className="text-text-muted">Wypełnij formularz — oddzwonimy i ustalimy termin wizyty</p>
</div>
{/* Pilne zgłoszenie toggle */}
<div
onClick={() => setIsUrgent(!isUrgent)}
className={`flex items-center gap-4 p-4 rounded-2xl border-2 cursor-pointer mb-6 transition-all ${
@@ -214,7 +211,6 @@ export function AwariaClient() {
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
{/* Sekcja 1: Urządzenie */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<Zap className="size-4 text-accent" /> Urządzenie
@@ -270,7 +266,6 @@ export function AwariaClient() {
</div>
</div>
{/* Sekcja 2: Problem */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<AlertTriangle className="size-4 text-accent" /> Opis problemu
@@ -293,7 +288,6 @@ export function AwariaClient() {
</div>
</div>
{/* Kod błędu z podpowiedzią */}
<div>
<label className="text-xs font-medium text-primary mb-1 block">
Kod błędu z wyświetlacza (opcjonalnie)
@@ -328,7 +322,6 @@ export function AwariaClient() {
{/* Sekcja 4: Dane kontaktowe */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<Phone className="size-4 text-accent" /> Dane kontaktowe
@@ -367,7 +360,6 @@ export function AwariaClient() {
</div>
</div>
{/* Zgoda RODO */}
<div
className="flex items-start gap-3 cursor-pointer"
onClick={() => setConsent(!consent)}
@@ -383,7 +375,6 @@ export function AwariaClient() {
</span>
</div>
{/* Submit */}
<button
type="submit"
disabled={isSubmitting || !brand || !problemType || !name || !phone || !city || !consent}
+48 -1
View File
@@ -15,6 +15,8 @@ const contactFormSchema = z.object({
city: z.string().min(1, "Wybierz miasto"),
subject: z.string().optional(),
message: z.string().optional(),
brand: z.string().optional(),
age: z.string().optional(),
});
type ContactFormValues = z.infer<typeof contactFormSchema>;
@@ -28,7 +30,8 @@ export function ContactForm() {
register,
handleSubmit: hookFormSubmit,
formState: { errors, isSubmitting },
reset
reset,
watch
} = useForm<ContactFormValues>({
resolver: zodResolver(contactFormSchema),
defaultValues: {
@@ -38,6 +41,8 @@ export function ContactForm() {
city: '',
subject: '',
message: '',
brand: '',
age: '',
}
});
@@ -185,11 +190,53 @@ export function ContactForm() {
<option value="">Wybierz temat...</option>
<option value="klimatyzacja">Klimatyzacja</option>
<option value="pompy-ciepla">Pompy ciepła</option>
<option value="serwis">Serwis</option>
<option value="inne">Inne zapytanie</option>
</select>
{errors.subject && <p className="text-red-500 text-xs font-medium">{errors.subject.message}</p>}
</div>
{/* Warunkowe pola dla serwisu */}
{watch('subject') === 'serwis' && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="space-y-2">
<label htmlFor="brand" className="text-sm font-semibold text-primary">Marka klimatyzatora</label>
<select
{...register("brand")}
id="brand"
className={`w-full px-4 py-3 rounded-xl border ${errors.brand ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 appearance-none`}
>
<option value="">Wybierz markę...</option>
<option value="Daikin">Daikin</option>
<option value="Mitsubishi">Mitsubishi</option>
<option value="LG">LG</option>
<option value="Samsung">Samsung</option>
<option value="Gree">Gree</option>
<option value="Rotenso">Rotenso</option>
<option value="Haier">Haier</option>
<option value="Aux">Aux</option>
<option value="Fuji Electric">Fuji Electric</option>
<option value="Toshiba">Toshiba</option>
<option value="Inna">Inna / Nie wiem</option>
</select>
</div>
<div className="space-y-2">
<label htmlFor="age" className="text-sm font-semibold text-primary">Wiek urządzenia</label>
<select
{...register("age")}
id="age"
className={`w-full px-4 py-3 rounded-xl border ${errors.age ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 appearance-none`}
>
<option value="">Jak długo masz klimatyzator?</option>
<option value="Mniej niż rok">Mniej niż rok (na gwarancji)</option>
<option value="1-3 lata">1 - 3 lata</option>
<option value="Ponad 3 lata">Ponad 3 lata</option>
<option value="Nie wiem">Nie wiem</option>
</select>
</div>
</div>
)}
{/* Wiadomość */}
<div className="space-y-2">
<label htmlFor="message" className="text-sm font-semibold text-primary">Wiadomość</label>
+15
View File
@@ -211,6 +211,21 @@ export function Header() {
>
Pompy ciepła
</Link>
<NavDropdown
label="Sklep"
triggerHref="/sklep"
sections={[
{
title: "Kategorie produktów",
items: [
{ label: "Klimatyzatory ścienne", href: "/sklep?category=scienne", description: "Jednostki do domu i biura" },
{ label: "Klimatyzatory multisplit", href: "/sklep?category=multisplit", description: "Systemy na kilka pomieszczeń" },
{ label: "Pompy ciepła", href: "/sklep?category=pompy-ciepla", description: "Ekologiczne ogrzewanie" },
]
}
]}
align="left"
/>
<NavDropdown
label="Miasta"
sections={miastaSections}
+27 -4
View File
@@ -53,7 +53,6 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
isOpen ? "pointer-events-auto" : "pointer-events-none"
)}
>
{/* Dark Overlay */}
<div
className={cn(
"absolute inset-0 bg-black/60 transition-opacity duration-300",
@@ -62,14 +61,12 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
onClick={onClose}
/>
{/* Drawer Panel */}
<div
className={cn(
"absolute top-0 right-0 bottom-0 w-full max-w-sm bg-white shadow-xl flex flex-col transition-transform duration-300 ease-in-out",
isOpen ? "translate-x-0" : "translate-x-full"
)}
>
{/* Drawer Header */}
<div className="flex justify-between items-center px-6 py-4 border-b border-border">
<span className="text-xl font-bold font-heading text-primary">
Menu<span className="text-accent">.</span>
@@ -83,7 +80,6 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
</button>
</div>
{/* Drawer Content */}
<div className="flex-1 overflow-y-auto px-6 py-4">
<div className="w-full">
{/* 1. Klimatyzacja */}
@@ -143,6 +139,33 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
</Link>
</div>
{/* Sklep */}
<details className="collapse collapse-arrow border-b border-border rounded-none">
<summary className="collapse-title text-sm font-medium text-primary hover:text-accent py-3 px-0 min-h-0">
Sklep
</summary>
<div className="collapse-content px-0 pb-2">
<ul className="flex flex-col gap-2.5 pl-4 pb-2">
{[
{ label: "Klimatyzatory ścienne", href: "/sklep?category=scienne" },
{ label: "Klimatyzatory multisplit", href: "/sklep?category=multisplit" },
{ label: "Pompy ciepła", href: "/sklep?category=pompy-ciepla" },
].map((item, idx) => (
<li key={idx}>
<Link
href={item.href}
onClick={onClose}
className="text-sm text-text-muted hover:text-accent transition-colors flex items-center justify-between py-1"
>
<span>{item.label}</span>
<ChevronRight className="size-3.5 opacity-60" />
</Link>
</li>
))}
</ul>
</div>
</details>
{/* Nowa sekcja: Miasta */}
<div className="border-b border-border py-3">
<details className="collapse collapse-arrow rounded-none">
+3 -3
View File
@@ -24,7 +24,7 @@ export function ContactSection() {
</div>
<div>
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Zadzwoń do nas</p>
<a href={`tel:${companyDetails.phoneRaw}`} className="text-2xl font-bold text-primary hover:text-accent transition-colors font-heading">
<a href={`tel:${companyDetails.phoneRaw}`} className="text-base font-bold text-primary hover:text-accent transition-colors font-heading">
{companyDetails.phone}
</a>
</div>
@@ -37,7 +37,7 @@ export function ContactSection() {
</div>
<div>
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Napisz e-mail</p>
<a href={`mailto:${companyDetails.email}`} className="text-xl font-bold text-primary hover:text-accent transition-colors font-heading">
<a href={`mailto:${companyDetails.email}`} className="text-base font-bold text-primary hover:text-accent transition-colors font-heading">
{companyDetails.email}
</a>
</div>
@@ -50,7 +50,7 @@ export function ContactSection() {
</div>
<div>
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Nasze biuro</p>
<div className="text-lg font-medium text-primary">
<div className="text-sm font-medium text-primary">
{companyDetails.name}<br />
{companyDetails.address.street}<br />
{companyDetails.address.zipCode} {companyDetails.address.city}
+4 -3
View File
@@ -1,6 +1,7 @@
import Link from "next/link";
import Image from "next/image";
import { ArrowRight, Star } from "lucide-react";
import { buttonClasses } from "@/components/ui/Button";
export interface HeroSectionProps {
eyebrow?: string;
@@ -31,7 +32,7 @@ export function HeroSection({
features
}: HeroSectionProps = {}) {
return (
<section className="relative bg-primary min-h-[480px] md:min-h-[600px] flex items-center overflow-hidden max-w-[2560px] mx-auto">
<section className="relative bg-primary min-h-[480px] md:min-h-[600px] flex items-center overflow-hidden w-full">
{/* TODO ZDJĘCIE HERO: realne zdjęcie firmy/montażu, format poziomy min. 1920×1080, plik public/hero.jpg */}
<Image
src={backgroundImage}
@@ -80,13 +81,13 @@ export function HeroSection({
{/* CTA Buttons */}
<div className="mt-10 flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto">
{ctaText && ctaLink && (
<Link href={ctaLink} className="w-fit sm:w-auto group btn btn-primary text-white font-bold px-8">
<Link href={ctaLink} className={buttonClasses({ variant: 'primary', size: 'md', className: 'w-fit sm:w-auto group' })}>
<span>{ctaText}</span>
<ArrowRight className="size-5 group-hover:translate-x-1 transition-transform" />
</Link>
)}
{secondaryCtaText && secondaryCtaLink && (
<Link href={secondaryCtaLink} className="w-fit sm:w-auto btn btn-outline bg-white/10 text-white border-white/20 hover:bg-white hover:text-primary font-bold px-8">
<Link href={secondaryCtaLink} className={buttonClasses({ variant: 'outline', size: 'md', className: 'w-fit sm:w-auto' })}>
<span>{secondaryCtaText}</span>
</Link>
)}
-5
View File
@@ -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
-5
View File
@@ -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" />
-8
View File
@@ -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" />
-3
View File
@@ -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) => (
+9 -21
View File
@@ -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>
+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
@@ -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>
);
}
+12 -1
View File
@@ -3,7 +3,7 @@ export interface Product {
sku: string;
name: string;
brand: string;
category: 'scienne' | 'multisplit' | 'kanalowe' | 'konsolowe' | 'podlogowe' | 'jednostka-scienna';
category: 'scienne' | 'multisplit' | 'kanalowe' | 'konsolowe' | 'podlogowe' | 'jednostka-scienna' | 'pompy-ciepla';
price: number | null;
image: string;
shortDescription: string;
@@ -8889,6 +8889,17 @@ export const products: Product[] = [
shortDescription: 'Mitsubishi Electric MXZ – R32, inwerterowy agregat Multi Split, Wi-Fi MELCloud, czynnik R32, WiFi, grzanie do -15°C, 6 jednostek wewnętrzne. Do domów i biur.',
specs: {"Moc chłodnicza": "12.2", "Moc grzewcza (kW)": "14.0", "Czynnik chłodniczy": "R32", "Grzanie do": "'-15", "Moc chłodnicza min (kW)": "3.5", "Moc chłodnicza max (kW)": "13.5", "Moc grzewcza min (kW)": "3.5", "Liczba jednostek wewn.": "6.0"},
},
{
slug: 'panasonic-aquarea-high-performance',
sku: 'PANASONIC-AQUAREA-HP',
name: 'Pompa ciepła Panasonic Aquarea High Performance 9kW',
brand: 'Panasonic',
category: 'pompy-ciepla',
price: 24500,
image: '/images/products/placeholder.jpg',
shortDescription: 'Nowoczesna pompa ciepła typu split, idealna do nowych domów energooszczędnych. Wysoka wydajność, niskie koszty eksploatacji i sterowanie przez WiFi.',
specs: {"Moc grzewcza (kW)": "9", "Zasilanie": "1-fazowe", "Czynnik chłodniczy": "R32", "Klasa energetyczna": "A+++"},
}
];
export const getProducts = () => products;
+2 -2
View File
@@ -6,13 +6,13 @@ export const services: ServiceItem[] = [
title: "Montaż klimatyzacji",
description: "Profesjonalny dobór, montaż klimatyzacji split i multisplit dla domów, mieszkań i firm.",
icon: "Wind",
href: "/klimatyzacja",
href: "/montaz-klimatyzacji",
},
{
title: "Serwis i przeglądy",
description: "Regularne przeglądy, czyszczenie, odgrzybianie oraz serwis naprawczy urządzeń klimatyzacyjnych.",
icon: "Wrench",
href: "/klimatyzacja#serwis",
href: "/serwis-klimatyzacji",
},
{
title: "Pompy ciepła",
+29
View File
@@ -0,0 +1,29 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { Product } from '@/content/products';
interface RecentlyViewedState {
items: Product[];
addItem: (item: Product) => void;
clear: () => void;
}
export const useRecentlyViewedStore = create<RecentlyViewedState>()(
persist(
(set) => ({
items: [],
addItem: (newItem) => {
set((state) => {
// Usuń, jeśli już jest na liście (aby przenieść na początek)
const filtered = state.items.filter((i) => i.slug !== newItem.slug);
// Dodaj na początek, zachowaj tylko ostatnie 4
return { items: [newItem, ...filtered].slice(0, 4) };
});
},
clear: () => set({ items: [] }),
}),
{ name: 'thermcool-recently-viewed' }
)
);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 12 KiB