Files

941 lines
49 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect, useMemo } from 'react';
import Link from 'next/link';
import { ArrowRight, ArrowLeft, Check, Home, Zap, ShoppingCart, Phone } from 'lucide-react';
import { Product } from '@/content/products';
import { companyDetails } from '@/content/company';
import { ProductCard } from '@/components/shop/ProductCard';
interface Props {
products: Product[];
}
// Hook do zapisywania stanu w sessionStorage
function useSessionState<T>(key: string, initialValue: T) {
const [state, setState] = useState<T>(initialValue);
useEffect(() => {
try {
const item = window.sessionStorage.getItem(key);
if (item) {
const timer = setTimeout(() => setState(JSON.parse(item)), 0);
return () => clearTimeout(timer);
}
} catch {}
}, [key]);
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(state) : value;
setState(valueToStore);
if (typeof window !== 'undefined') {
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch {}
};
return [state, setValue] as const;
}
// Oblicz wymaganą moc chłodniczą w kW
function calculatePower(m2: number, floor: string, sun: string, building: string): number {
let power = m2 * 0.1; // baza: 1 kW na 10 m²
// Korekta typu budynku i piętra
if (building === 'apartment') {
if (floor === 'top') power *= 1.2; // ostatnie piętro - więcej ciepła z dachu
if (floor === 'ground') power *= 0.95;
} else if (building === 'house') {
if (floor === 'top') power *= 1.1; // poddasze - dodatkowe nagrzewanie
}
// Korekta nasłonecznienia
if (sun === 'south') power *= 1.15; // południe - duże nasłonecznienie
if (sun === 'west') power *= 1.1;
if (sun === 'north') power *= 0.9;
return Math.round(power * 10) / 10;
}
// Szacunkowy koszt montażu
function estimateInstall(pipes: string, rooms: number, installMethod: string, outdoorUnitLocation: string, hasSocket: string): { min: number; max: number } {
const base = rooms === 1 ? 1400 : rooms === 2 ? 2400 : rooms === 3 ? 3200 : 3800;
// Przybliżona liczba dodatkowych metrów (powyżej 3m)
const extraMeters = pipes === 'short' ? 0 : pipes === 'medium' ? 3 : pipes === 'long' ? 7 : 15;
const methodPrice = installMethod === 'bruzda' ? 90 : 80; // zł za metr
// Zostawmy bazowe koszty rur miedzianych (np. miedź, kable) i dodajmy koszt bruzdy/korytka
const pipeBaseCost = pipes === 'short' ? 0 : pipes === 'medium' ? 200 : pipes === 'long' ? 500 : 1000;
// Elewacja: 400 zł (pompka) + ok. 600 zł (1h podnośnika jako bufor)
const facadeCost = outdoorUnitLocation === 'facade' ? 1000 : 0;
// Brak gniazdka: dodatkowy koszt ok 200 zł za dociągnięcie zasilania
const socketCost = hasSocket !== 'yes' ? 200 : 0;
const pipeExtra = pipeBaseCost + (extraMeters * methodPrice) + facadeCost + socketCost;
return { min: base + pipeExtra, max: base + pipeExtra + 600 };
}
const STEPS = ['Typ', 'Pomieszczenie', 'Instalacja', 'Budżet', 'Dobór'] as const;
export function KalkulatorClient({ products }: Props) {
const [showWidthWarning, setShowWidthWarning] = useState(false);
const [maxWidth, setMaxWidth] = useState(80);
const [deviceType, setDeviceType] = useSessionState<'klimatyzacja' | 'pompa'>('calc_deviceType', 'klimatyzacja');
const [pumpFormSent, setPumpFormSent] = useState(false);
const [pumpForm, setPumpForm] = useState({ name: '', phone: '', city: '', message: '' });
const [step, setStep] = useSessionState('calc_step', 0);
const [building, setBuilding] = useSessionState('calc_building', 'apartment');
const [pomieszczenia, setPomieszczenia] = useSessionState<{ nazwa: string; m2: number }[]>('calc_pomieszczenia', [
{ nazwa: 'Pokój 1', m2: 20 }
]);
const [floor, setFloor] = useSessionState('calc_floor', 'middle'); // ground | middle | top
const [sun, setSun] = useSessionState('calc_sun', 'south'); // north | east | south | west
const addRoom = () => {
if (pomieszczenia.length < 5) {
setPomieszczenia(prev => [...prev, { nazwa: `Pokój ${prev.length + 1}`, m2: 15 }]);
}
};
const removeRoom = (i: number) => {
if (pomieszczenia.length > 1) {
setPomieszczenia(prev => prev.filter((_, idx) => idx !== i));
}
};
const updateRoom = (i: number, m2: number) => {
setPomieszczenia(prev => prev.map((r, idx) => idx === i ? { ...r, m2 } : r));
};
const [pipes, setPipes] = useSessionState('calc_pipes', 'medium'); // short(<3m) | medium(3-6m) | long(6-10m) | verylong(>10m)
const [installMethod, setInstallMethod] = useSessionState<'korytko' | 'bruzda'>('calc_installMethod', 'korytko');
const [outdoorUnitLocation, setOutdoorUnitLocation] = useSessionState<'balcony' | 'facade' | 'roof'>('calc_outdoorLocation', 'balcony');
const [hasSocket, setHasSocket] = useSessionState<'yes' | 'no' | 'dont_know'>('calc_hasSocket', 'yes');
const [budget, setBudget] = useSessionState('calc_budget', 8000);
const [preferredBrand, setPreferredBrand] = useSessionState('calc_brand', '');
const [energyClass, setEnergyClass] = useSessionState('calc_energy', '');
const [maxNoise, setMaxNoise] = useSessionState<number | null>('calc_maxNoise', null);
const [maxNoiseOut, setMaxNoiseOut] = useSessionState<number | null>('calc_maxNoiseOut', null);
const [page, setPage] = useSessionState('calc_page', 1);
const totalM2 = pomieszczenia.reduce((sum, p) => sum + p.m2, 0);
const rooms = pomieszczenia.length;
const requiredPower = useMemo(() =>
pomieszczenia.reduce((sum, p) => sum + calculatePower(p.m2, floor, sun, building), 0),
[pomieszczenia, floor, sun, building]
);
const requiredPowerRounded = Math.round(requiredPower * 10) / 10;
const installCost = useMemo(() => estimateInstall(pipes, rooms, installMethod, outdoorUnitLocation, hasSocket), [pipes, rooms, installMethod, outdoorUnitLocation, hasSocket]);
const category = rooms === 1 ? 'scienne' : 'multisplit';
const results = useMemo(() => {
if (step < 4 || deviceType === 'pompa') return [];
const maxPrice = budget * 0.75;
const powerMin = requiredPower * 0.6;
const powerMax = requiredPower * 1.8;
const filtered = products
.filter(p => {
if (p.category !== category) return false;
if (preferredBrand && p.brand !== preferredBrand) return false;
if (energyClass) {
const pClass = p.specs?.['Klasa energetyczna'];
if (!pClass || !pClass.includes(energyClass)) return false;
}
if (maxNoise !== null) {
const noiseStr = p.specs?.['Hałas wewnątrz (dB)'];
if (!noiseStr) return false;
// Hałas jest zapisany np. "19–38" - bierzemy dolną granicę
const minNoise = parseInt(noiseStr.split('–')[0].trim());
if (isNaN(minNoise) || minNoise > maxNoise) return false;
}
if (maxNoiseOut !== null) {
const noiseOutStr = p.specs?.['Hałas zewnątrz (dB)'];
// Jeśli produktu nie ma tego parametru, odrzucamy, jeśli nałożono ścisły filtr
if (!noiseOutStr) return false;
const minNoiseOut = parseInt(noiseOutStr.split('–')[0].trim());
if (isNaN(minNoiseOut) || minNoiseOut > maxNoiseOut) return false;
}
if (category === 'multisplit') {
if (p.price !== null && p.price > maxPrice) return false;
} else {
if (p.price === null || p.price > maxPrice) return false;
}
const moc = parseFloat(String(p.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]);
return moc >= powerMin && moc <= powerMax;
})
.sort((a, b) => {
// Sortuj po tym jak blisko jest moc do wymaganej
const mocA = parseFloat(String(a.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]);
const mocB = parseFloat(String(b.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]);
return Math.abs(mocA - requiredPower) - Math.abs(mocB - requiredPower);
});
return filtered.length > 0
? filtered
: products
.filter(p => p.category === category && p.price !== null)
.sort((a, b) => (a.price || 0) - (b.price || 0));
}, [step, deviceType, products, requiredPower, category, budget, preferredBrand, energyClass, maxNoise, maxNoiseOut]);
const paginatedResults = useMemo(() => results.slice((page - 1) * 9, page * 9), [results, page]);
const totalPages = Math.ceil(results.length / 9);
const getPageNumbers = () => {
const pages = [];
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
} else {
if (page <= 4) {
pages.push(1, 2, 3, 4, 5, '...', totalPages);
} else if (page >= totalPages - 3) {
pages.push(1, '...', totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages);
} else {
pages.push(1, '...', page - 1, page, page + 1, '...', totalPages);
}
}
return pages;
};
const handleNext = () => {
setStep(s => s + 1);
setPage(1);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handlePumpSubmit = async () => {
if (!pumpForm.name || !pumpForm.phone || !pumpForm.city) return;
// Na razie pokazujemy success — podepniemy Resend później
setPumpFormSent(true);
};
return (
<main className="bg-slate-50 min-h-screen py-12">
<div className={`mx-auto px-4 sm:px-6 transition-all duration-500 ${step === 4 && deviceType !== 'pompa' ? 'max-w-5xl' : 'max-w-2xl'}`}>
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-3xl font-extrabold text-primary font-heading mb-2">
Dobierz urządzenie
</h1>
<p className="text-text-muted">Odpowiedz na {STEPS.length} krótkich pytań — pokażemy Ci najlepsze opcje z naszej oferty</p>
</div>
{/* Pasek postępu */}
<div className="flex items-center mb-8">
{STEPS.map((s, i) => (
<div key={s} className="flex items-start flex-1 last:flex-none">
<div className="flex flex-col items-center gap-1">
<div className={`size-7 rounded-full flex items-center justify-center text-xs font-bold shrink-0 transition-all ${
i < step ? 'bg-green-500 text-white' : i === step ? 'bg-accent text-white' : 'bg-slate-200 text-text-muted'
}`}>
{i < step ? <Check className="size-3.5" /> : i + 1}
</div>
<span className={`text-xs font-medium hidden sm:block whitespace-nowrap ${i === step ? 'text-accent' : 'text-text-muted'}`}>{s}</span>
</div>
{i < STEPS.length - 1 && (
<div className={`flex-1 h-0.5 mx-2 mt-[13px] ${i < step ? 'bg-green-500' : 'bg-slate-200'}`} />
)}
</div>
))}
</div>
{/* Karty kroków */}
<div className="bg-white rounded-2xl border border-border p-6 md:p-8">
{/* Krok 0: Typ urządzenia */}
{step === 0 && (
<div className="flex flex-col gap-4">
<h2 className="text-lg font-bold text-primary font-heading mb-2">Co chcesz zainstalować?</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button onClick={() => setDeviceType('klimatyzacja')}
className={`p-5 rounded-xl border-2 text-left transition-all ${deviceType === 'klimatyzacja' ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<span className="text-3xl mb-2 block">❄️</span>
<p className="font-bold text-primary">Klimatyzacja</p>
<p className="text-xs text-text-muted mt-1">Chłodzenie latem, opcjonalne grzanie zimą</p>
</button>
<button onClick={() => setDeviceType('pompa')}
className={`p-5 rounded-xl border-2 text-left transition-all ${deviceType === 'pompa' ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<span className="text-3xl mb-2 block">🔥</span>
<p className="font-bold text-primary">Pompa ciepła</p>
<p className="text-xs text-text-muted mt-1">Głównie ogrzewanie domu z niskim kosztem eksploatacji</p>
</button>
</div>
{deviceType === 'pompa' && !pumpFormSent && (
<div className="mt-4 p-5 bg-white border border-border rounded-2xl flex flex-col gap-4">
<p className="text-sm font-bold text-primary">Wypełnij formularz — oddzwonimy w celu ustalenia terminu bezpłatnej i niezobowiązującej wizji lokalnej</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Imię i nazwisko *</label>
<input
name="pump_name" required
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
placeholder="Jan Kowalski"
value={pumpForm.name}
onChange={e => setPumpForm(f => ({...f, name: e.target.value}))}
/>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Telefon *</label>
<input
name="pump_phone" required type="tel"
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
placeholder="+48 600 000 000"
value={pumpForm.phone}
onChange={e => setPumpForm(f => ({...f, phone: e.target.value}))}
/>
</div>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Miasto *</label>
<input
name="pump_city" required
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
placeholder="Wrocław"
value={pumpForm.city}
onChange={e => setPumpForm(f => ({...f, city: e.target.value}))}
/>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Krótki opis (opcjonalnie)</label>
<textarea
name="pump_message" rows={3}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none resize-none"
placeholder="Np. dom 150m², nowy budynek, chcę wymienić ogrzewanie gazowe..."
value={pumpForm.message}
onChange={e => setPumpForm(f => ({...f, message: e.target.value}))}
/>
</div>
<button
onClick={handlePumpSubmit}
disabled={!pumpForm.name || !pumpForm.phone || !pumpForm.city}
className="flex items-center justify-center gap-2 py-3 px-6 bg-accent text-white rounded-xl font-bold text-sm hover:bg-accent/90 transition-colors disabled:opacity-50"
>
Wyślij zapytanie o pompę ciepła
</button>
<p className="text-xs text-text-muted text-center mt-2">
Wizja lokalna jest bezpłatna i niezobowiązująca. Oddzwaniamy w ciągu 24 godzin.
</p>
</div>
)}
{deviceType === 'pompa' && pumpFormSent && (
<div className="mt-4 p-5 bg-green-50 border border-green-200 rounded-2xl text-center">
<p className="text-green-700 font-bold mb-1">✅ Zapytanie wysłane!</p>
<p className="text-sm text-green-600">Skontaktujemy się z Tobą w ciągu 24 godzin z bezpłatną wyceną.</p>
</div>
)}
</div>
)}
{/* Krok 1: Pomieszczenie */}
{step === 1 && (
<div className="flex flex-col gap-6">
<div className="flex items-center gap-3 mb-2">
<div className="size-10 rounded-xl bg-blue-50 flex items-center justify-center">
<Home className="size-5 text-blue-600" />
</div>
<div>
<h2 className="text-lg font-bold text-primary font-heading">Pomieszczenie</h2>
<p className="text-xs text-text-muted">Metraż i warunki</p>
</div>
</div>
{/* Lista pomieszczeń */}
<div className="flex flex-col gap-3">
{pomieszczenia.map((p, i) => (
<div key={i} className="bg-slate-50 rounded-xl border border-border p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-primary">{p.nazwa}</span>
{pomieszczenia.length > 1 && (
<button
onClick={() => removeRoom(i)}
className="text-xs text-red-400 hover:text-red-600 transition-colors"
>
Usuń
</button>
)}
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<span className="text-xs text-text-muted">Powierzchnia</span>
<span className="text-xs font-bold text-primary bg-accent/10 px-2 py-0.5 rounded-md">{p.m2} m²</span>
</div>
<input
type="range" min={5} max={100} step={5} value={p.m2}
onChange={e => updateRoom(i, Number(e.target.value))}
className="range range-xs range-accent w-full"
/>
<div className="flex justify-between text-xs text-text-muted">
<span>5 m²</span>
<span>100 m²</span>
</div>
{(() => {
const mocPokoju = Math.round(calculatePower(p.m2, floor, sun, building) * 10) / 10;
const dostepneMoce = [2.5, 3.5, 5.0, 7.0];
const zalecana = dostepneMoce.reduce((prev, curr) =>
Math.abs(curr - mocPokoju) < Math.abs(prev - mocPokoju) ? curr : prev
);
return (
<div className="mt-2 flex items-center justify-between bg-white border border-border rounded-lg px-3 py-2">
<span className="text-xs text-text-muted">Zalecana moc jednostki</span>
<span className="text-sm font-bold text-accent">{zalecana} kW</span>
</div>
);
})()}
</div>
</div>
))}
</div>
{pomieszczenia.length < 5 && (
<button
onClick={addRoom}
className="flex items-center gap-2 text-sm text-accent font-semibold hover:text-accent/80 transition-colors border border-dashed border-accent/40 rounded-xl px-4 py-3 w-full justify-center"
>
+ Dodaj kolejne pomieszczenie
</button>
)}
{/* Ograniczenie szerokości */}
<div className="bg-slate-50 border border-border rounded-xl p-4">
<div className="flex items-start justify-between gap-3 mb-2">
<div>
<p className="text-sm font-semibold text-primary">Ograniczenie szerokości jednostki?</p>
<p className="text-xs text-text-muted mt-0.5">Np. wąska ściana, wnęka, szafa</p>
</div>
<button
onClick={() => setShowWidthWarning(prev => !prev)}
className={`shrink-0 text-xs font-bold px-3 py-1.5 rounded-lg transition-colors ${
showWidthWarning ? 'bg-amber-100 text-amber-700' : 'bg-slate-200 text-slate-600 hover:bg-slate-300'
}`}
>
{showWidthWarning ? 'Tak, mam ograniczenie' : 'Nie dotyczy'}
</button>
</div>
{showWidthWarning && (
<div className="mt-3">
<div className="flex items-center justify-between mb-1.5">
<span className="text-xs text-text-muted">Maksymalna szerokość jednostki</span>
<span className="text-xs font-bold text-amber-700 bg-amber-100 px-2 py-0.5 rounded-md">{maxWidth} cm</span>
</div>
<div className="relative h-5 flex items-center">
<div className="absolute w-full h-1.5 bg-slate-200 rounded-full" />
<div
className="absolute h-1.5 bg-amber-400 rounded-full"
style={{ width: `${((maxWidth - 40) / 80) * 100}%` }}
/>
<input
type="range" min={40} max={120} step={5} value={maxWidth}
onChange={e => setMaxWidth(Number(e.target.value))}
className="absolute w-full h-1.5 opacity-0 cursor-pointer z-10"
/>
<div
className="absolute size-4 bg-white border-2 border-amber-400 rounded-full shadow-md pointer-events-none"
style={{ left: `calc(${((maxWidth - 40) / 80) * 100}% - 8px)` }}
/>
</div>
<div className="flex justify-between text-xs text-text-muted mt-1">
<span>40 cm</span>
<span>120 cm</span>
</div>
<div className="mt-3 flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-xl">
<span className="text-amber-500 text-base shrink-0">⚠️</span>
<p className="text-xs text-amber-800 leading-relaxed">
Standardowe jednostki split mają <strong>70–110 cm szerokości</strong>.
{maxWidth < 70 && ' Przy tak małej przestrzeni wybór jest bardzo ograniczony — '}
{maxWidth >= 70 && maxWidth < 85 && ' Przy tej szerokości dostępne są kompaktowe modele — '}
{maxWidth >= 85 && ' Większość modeli powinna się zmieścić — '}
skonsultuj wymiary konkretnego modelu przed zakupem.
<a href="/kontakt" className="ml-1 font-bold text-amber-700 underline">Zapytaj nas →</a>
</p>
</div>
</div>
)}
</div>
<div className="p-3 bg-accent/10 rounded-xl">
<p className="text-sm text-accent font-semibold">
Łączna wymagana moc: ok. <strong>{requiredPowerRounded} kW</strong>
<span className="text-xs font-normal text-text-muted ml-2">
({pomieszczenia.length === 1 ? 'klimatyzator split' : `multisplit ${pomieszczenia.length}+1`}, łącznie {totalM2} m²)
</span>
</p>
</div>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">Typ budynku</label>
<div className="grid grid-cols-2 gap-2">
{[
{ value: 'apartment', label: '🏢 Mieszkanie w bloku', sub: 'Kamienica, wieżowiec, blok' },
{ value: 'house', label: '🏠 Dom jednorodzinny', sub: 'Wolnostojący, szeregowiec, bliźniak' },
].map(o => (
<button key={o.value} onClick={() => setBuilding(o.value)}
className={`p-4 rounded-xl border-2 text-left transition-all ${building === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<p className="text-sm font-bold text-primary">{o.label}</p>
<p className="text-xs text-text-muted">{o.sub}</p>
</button>
))}
</div>
</div>
{building === 'house' && (
<div>
<label className="text-sm font-semibold text-primary mb-3 block">Typ przestrzeni</label>
<div className="grid grid-cols-2 gap-2">
{[
{ value: 'ground', label: '🏠 Parter' },
{ value: 'top', label: '🏠 Piętro / poddasze' },
].map(o => (
<button key={o.value} onClick={() => setFloor(o.value)}
className={`py-2.5 px-3 rounded-xl text-sm font-semibold border-2 transition-all ${floor === o.value ? 'border-accent bg-accent text-white' : 'border-border bg-white text-primary hover:border-accent'}`}>
{o.label}
</button>
))}
</div>
</div>
)}
{building === 'apartment' && (
<div>
<label className="text-sm font-semibold text-primary mb-3 block">Piętro</label>
<div className="flex flex-wrap gap-2">
{[
{ value: 'ground', label: 'Parter' },
{ value: 'low', label: '1-3 piętro' },
{ value: 'middle', label: '4-8 piętro' },
{ value: 'high', label: '9+ piętro' },
{ value: 'top', label: 'Ostatnie' },
].map(o => (
<button key={o.value} onClick={() => setFloor(o.value)}
className={`py-2 px-4 rounded-xl text-sm font-semibold border-2 transition-all ${floor === o.value ? 'border-accent bg-accent text-white' : 'border-border bg-white text-primary hover:border-accent'}`}>
{o.label}
</button>
))}
</div>
</div>
)}
<div>
<label className="text-sm font-semibold text-primary mb-3 block">Ekspozycja okien</label>
<div className="grid grid-cols-2 gap-2">
{[
{ value: 'north', label: '🧭 Północ (chłodna)' },
{ value: 'east', label: '🧭 Wschód' },
{ value: 'west', label: '🧭 Zachód' },
{ value: 'south', label: '🧭 Południe (mocne słońce)' },
].map(o => (
<button key={o.value} onClick={() => setSun(o.value)}
className={`py-2.5 px-3 rounded-xl text-sm font-semibold border-2 transition-all ${sun === o.value ? 'border-accent bg-accent text-white' : 'border-border bg-white text-primary hover:border-accent'}`}>
{o.label}
</button>
))}
</div>
</div>
</div>
)}
{/* Krok 2: Instalacja */}
{step === 2 && (
<div className="flex flex-col gap-6">
<div className="flex items-center gap-3 mb-2">
<div className="size-10 rounded-xl bg-orange-50 flex items-center justify-center">
<Zap className="size-5 text-orange-600" />
</div>
<div>
<h2 className="text-lg font-bold text-primary font-heading">Długość instalacji</h2>
<p className="text-xs text-text-muted">Rury i kable między jednostkami</p>
</div>
</div>
<div className="flex flex-col gap-3">
{[
{ value: 'short', label: 'Do 3 metrów', sub: 'Jednostka zewnętrzna tuż obok (balkon, taras)' },
{ value: 'medium', label: '3–6 metrów', sub: 'Typowa instalacja w bloku lub domu' },
{ value: 'long', label: '6–10 metrów', sub: 'Dłuższa trasa, np. przez kilka pomieszczeń' },
{ value: 'verylong', label: 'Powyżej 10 metrów', sub: 'Bardzo długa trasa, wymaga dodatkowych materiałów' },
].map(o => (
<button key={o.value} onClick={() => setPipes(o.value)}
className={`p-4 rounded-xl border-2 text-left transition-all ${pipes === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<p className="text-sm font-bold text-primary">{o.label}</p>
<p className="text-xs text-text-muted">{o.sub}</p>
</button>
))}
</div>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">Sposób prowadzenia instalacji (powyżej 3m)</label>
<div className="flex flex-col gap-3">
{[
{ value: 'korytko', label: 'Korytko PCV', sub: 'Rury schowane w białym korytku (80 zł/mb)' },
{ value: 'bruzda', label: 'Bruzda w ścianie', sub: 'Rury ukryte w wykutej bruzdzie (90 zł/mb)' },
].map(o => (
<button key={o.value} onClick={() => setInstallMethod(o.value as 'korytko' | 'bruzda')}
className={`p-4 rounded-xl border-2 text-left transition-all ${installMethod === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<p className="text-sm font-bold text-primary">{o.label}</p>
<p className="text-xs text-text-muted">{o.sub}</p>
</button>
))}
</div>
</div>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">Miejsce montażu jednostki zewnętrznej (agregatu)</label>
<div className="flex flex-col gap-3">
{[
{ value: 'balcony', label: 'Zwykły balkon / taras / posadzka', sub: 'Standardowy montaż, najtańszy i najwygodniejszy w serwisowaniu' },
{ value: 'facade', label: 'Elewacja / balkon francuski', sub: 'Wymaga użycia podnośnika koszowego (ok. 600 zł/h) i pompki skroplin (+ ok. 400 zł)' },
{ value: 'roof', label: 'Dach budynku', sub: 'Wymaga odpowiedniego stelaża lub konstrukcji dachowej' },
].map(o => (
<button key={o.value} onClick={() => setOutdoorUnitLocation(o.value as 'balcony' | 'facade' | 'roof')}
className={`p-4 rounded-xl border-2 text-left transition-all ${outdoorUnitLocation === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<p className="text-sm font-bold text-primary">{o.label}</p>
<p className="text-xs text-text-muted">{o.sub}</p>
</button>
))}
</div>
{building === 'apartment' && outdoorUnitLocation !== 'balcony' && (
<div className="mt-3 p-3 bg-amber-50 rounded-xl text-xs text-amber-800 border border-amber-200">
⚠️ <strong>Pamiętaj:</strong> Montaż agregatu na elewacji lub dachu w bloku wielorodzinnym wymaga uzyskania pisemnej zgody spółdzielni, wspólnoty mieszkaniowej lub zarządcy budynku.
</div>
)}
</div>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">Zasilanie (gniazdko lub prąd w miejscu montażu agregatu)</label>
<div className="flex flex-col gap-3">
{[
{ value: 'yes', label: 'Tak', sub: 'Mam gniazdko na balkonie lub wyprowadzony kabel' },
{ value: 'no', label: 'Nie', sub: 'Instalator musi dociągnąć prąd z wewnątrz (+ ok. 200 zł)' },
{ value: 'dont_know', label: 'Nie wiem / Do ustalenia', sub: 'Do ustalenia w trakcie wizji lokalnej' },
].map(o => (
<button key={o.value} onClick={() => setHasSocket(o.value as 'yes' | 'no' | 'dont_know')}
className={`p-4 rounded-xl border-2 text-left transition-all ${hasSocket === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<p className="text-sm font-bold text-primary">{o.label}</p>
<p className="text-xs text-text-muted">{o.sub}</p>
</button>
))}
</div>
</div>
<div className="p-3 bg-orange-50 rounded-xl text-xs text-orange-700">
Szacowany koszt montażu dla Twojej konfiguracji:
<strong className="ml-1">{installCost.min.toLocaleString('pl-PL')}–{installCost.max.toLocaleString('pl-PL')} zł</strong>
</div>
<div className="mt-3 p-4 bg-blue-50 border border-blue-200 rounded-xl">
<p className="text-sm font-semibold text-blue-900 mb-1">📐 Chcesz dokładną wycenę?</p>
<p className="text-xs text-blue-800 leading-relaxed">
Skontaktuj się z nami — wycena jest <strong>bezpłatna i niezobowiązująca</strong>.
Aby przyspieszyć wycenę, wyślij nam zdjęcia:
</p>
<ul className="mt-2 flex flex-col gap-1">
{[
'Miejsca gdzie ma wisieć jednostka wewnętrzna',
'Ściany przez które przejdzie instalacja',
'Miejsca gdzie stanie jednostka zewnętrzna',
'Skrzynki elektrycznej (bezpieczniki)',
].map((item, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-blue-800">
<span className="text-blue-500 shrink-0">→</span>
{item}
</li>
))}
</ul>
<a href="/kontakt" className="mt-3 inline-flex items-center gap-1.5 text-xs font-bold text-white bg-accent px-3 py-1.5 rounded-lg hover:bg-accent/90 transition-colors">
Wyślij zdjęcia i zapytaj o wycenę →
</a>
</div>
</div>
)}
{/* Krok 3: Budżet */}
{step === 3 && (
<div className="flex flex-col gap-6">
<div className="flex items-center gap-3 mb-2">
<div className="size-10 rounded-xl bg-green-50 flex items-center justify-center">
<ShoppingCart className="size-5 text-green-600" />
</div>
<div>
<h2 className="text-lg font-bold text-primary font-heading">Całkowity budżet</h2>
<p className="text-xs text-text-muted">Urządzenie + montaż łącznie</p>
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<span className="text-xs text-text-muted">Budżet</span>
<span className="text-xs font-bold text-primary bg-accent/10 px-2 py-0.5 rounded-md">{budget.toLocaleString('pl-PL')} zł</span>
</div>
<input
type="range" min={3000} max={25000} step={500} value={budget}
onChange={e => setBudget(Number(e.target.value))}
className="range range-xs range-accent w-full"
/>
<div className="flex justify-between text-xs text-text-muted">
<span>3 000 zł</span>
<span>25 000 zł</span>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-xl p-3">
<p className="text-xs text-text-muted mb-1">Na urządzenie zostaje</p>
<p className="text-base font-bold text-primary font-heading">
ok. {(budget * 0.75).toLocaleString('pl-PL')} zł
</p>
</div>
<div className="bg-orange-50 rounded-xl p-3">
<p className="text-xs text-orange-700 mb-1">Szac. koszt montażu</p>
<p className="text-base font-bold text-orange-700 font-heading">
{installCost.min.toLocaleString('pl-PL')}–{installCost.max.toLocaleString('pl-PL')} zł
</p>
</div>
</div>
<div className="p-3 bg-blue-50 rounded-xl text-xs text-blue-700">
💡 Podsumowanie: szukamy klimatyzatora <strong>{category === 'scienne' ? 'split ściennego' : 'multisplit'}</strong> o mocy ok. <strong>{requiredPowerRounded} kW</strong> w cenie do <strong>{(budget * 0.75).toLocaleString('pl-PL')} zł</strong>
</div>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">
Preferowana marka (opcjonalnie)
</label>
<div className="flex flex-wrap gap-2">
{['Bez preferencji', 'Gree', 'Rotenso', 'Daikin', 'Haier', 'LG', 'Samsung', 'Mitsubishi Electric', 'Toshiba', 'Fuji Electric'].map(b => (
<button
key={b}
onClick={() => setPreferredBrand(b === 'Bez preferencji' ? '' : b)}
className={`px-3 py-1.5 rounded-xl text-xs font-semibold border-2 transition-all ${
(b === 'Bez preferencji' && preferredBrand === '') || preferredBrand === b
? 'border-accent bg-accent text-white'
: 'border-border bg-white text-primary hover:border-accent'
}`}
>
{b}
</button>
))}
</div>
</div>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">
Klasa energetyczna (opcjonalnie)
</label>
<div className="flex flex-wrap gap-2">
{['Bez preferencji', 'A+++', 'A++', 'A+'].map(c => (
<button
key={c}
onClick={() => setEnergyClass(c === 'Bez preferencji' ? '' : c)}
className={`px-3 py-1.5 rounded-xl text-xs font-semibold border-2 transition-all ${
(c === 'Bez preferencji' && energyClass === '') || energyClass === c
? 'border-accent bg-accent text-white'
: 'border-border bg-white text-primary hover:border-accent'
}`}
>
{c}
</button>
))}
</div>
</div>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">
Maksymalna głośność wewnątrz (opcjonalnie)
</label>
<div className="flex flex-wrap gap-2">
{[
{ label: 'Bez znaczenia', val: null },
{ label: 'Bardzo ciche (do 20 dB)', val: 20 },
{ label: 'Ciche (do 25 dB)', val: 25 },
{ label: 'Standardowe (do 30 dB)', val: 30 }
].map(n => (
<button
key={n.label}
onClick={() => setMaxNoise(n.val)}
className={`px-3 py-1.5 rounded-xl text-xs font-semibold border-2 transition-all ${
maxNoise === n.val
? 'border-accent bg-accent text-white'
: 'border-border bg-white text-primary hover:border-accent'
}`}
>
{n.label}
</button>
))}
</div>
</div>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">
Maksymalna głośność zewnątrz (opcjonalnie)
</label>
<div className="flex flex-wrap gap-2">
{[
{ label: 'Bez znaczenia', val: null },
{ label: 'Bardzo ciche (do 45 dB)', val: 45 },
{ label: 'Ciche (do 50 dB)', val: 50 },
{ label: 'Standardowe (do 55 dB)', val: 55 }
].map(n => (
<button
key={n.label}
onClick={() => setMaxNoiseOut(n.val)}
className={`px-3 py-1.5 rounded-xl text-xs font-semibold border-2 transition-all ${
maxNoiseOut === n.val
? 'border-accent bg-accent text-white'
: 'border-border bg-white text-primary hover:border-accent'
}`}
>
{n.label}
</button>
))}
</div>
</div>
</div>
)}
{/* Krok 4: Wyniki */}
{step === 4 && deviceType === 'pompa' && (
<div className="flex flex-col gap-6 text-center">
<div className="bg-white rounded-2xl p-8">
<h2 className="text-2xl font-extrabold text-primary font-heading mb-4">
Wycena pompy ciepła
</h2>
<p className="text-text-muted mb-8 text-sm">
Każda instalacja pompy ciepła wymaga indywidualnego doboru urządzenia do specyfiki budynku (izolacja, instalacja grzewcza).
Skontaktuj się z nami, a przygotujemy dla Ciebie <strong>bezpłatną, spersonalizowaną ofertę</strong>.
</p>
<a href={`tel:${companyDetails.phoneRaw}`}>
<button className="flex items-center gap-2 px-6 py-3 bg-accent text-white rounded-xl font-bold mx-auto hover:bg-accent/90 transition-colors">
<Phone className="size-4" /> {companyDetails.phone}
</button>
</a>
</div>
</div>
)}
{step === 4 && deviceType !== 'pompa' && (
<div className="flex flex-col gap-6">
{/* Podsumowanie */}
<div className="bg-slate-50 rounded-2xl border border-border p-5">
<h2 className="text-lg font-bold text-primary font-heading mb-4">
Dopasowane klimatyzatory
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{[
{ label: 'Wymagana moc', value: `${requiredPowerRounded} kW` },
{ label: 'Powierzchnia', value: `${totalM2} m²` },
{ label: 'Typ instalacji', value: category === 'scienne' ? 'Split (1 pokój)' : `Multisplit (${rooms} pokoje)` },
{ label: 'Szac. montaż', value: `${installCost.min.toLocaleString('pl-PL')}–${installCost.max.toLocaleString('pl-PL')} zł` },
...(energyClass ? [{ label: 'Klasa energetyczna', value: energyClass }] : []),
...(maxNoise ? [{ label: 'Max. hałas wewn.', value: `do ${maxNoise} dB` }] : []),
...(maxNoiseOut ? [{ label: 'Max. hałas zewn.', value: `do ${maxNoiseOut} dB` }] : []),
].map((s, i) => (
<div key={i} className="bg-white rounded-xl p-3 border border-border">
<p className="text-xs text-text-muted mb-1">{s.label}</p>
<p className="text-sm font-bold text-primary font-heading">{s.value}</p>
</div>
))}
</div>
</div>
{results.length > 0 ? (
<div className="flex flex-col gap-8">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-5">
{paginatedResults.map(product => (
<ProductCard key={product.slug} product={product} />
))}
</div>
{totalPages > 1 && (
<div className="flex flex-wrap items-center justify-center gap-2">
{getPageNumbers().map((p, i) => (
p === '...' ? (
<span key={i} className="px-2 text-text-muted font-bold">...</span>
) : (
<button
key={i}
onClick={() => {
setPage(p as number);
// Drobne przewinięcie na początek wyników dopasowania po zmianie strony
window.scrollTo({ top: 150, behavior: 'smooth' });
}}
className={`size-10 flex items-center justify-center rounded-xl font-bold text-sm transition-colors ${
page === p ? 'bg-accent text-white' : 'bg-white border border-border text-primary hover:border-accent'
}`}
>
{p}
</button>
)
))}
</div>
)}
</div>
) : (
<div className="bg-slate-50 rounded-2xl border border-border p-8 text-center">
<p className="text-text-muted mb-4 text-sm">
Nie znaleziono produktów dla podanych kryteriów. Spróbuj zwiększyć budżet lub zadzwoń do nas — dobierzemy ofertę indywidualnie.
</p>
<a href={`tel:${companyDetails.phoneRaw}`}>
<button className="flex items-center gap-2 px-5 py-2.5 bg-accent text-white rounded-xl font-bold mx-auto hover:bg-accent/90 transition-colors text-sm">
<Phone className="size-4" /> {companyDetails.phone}
</button>
</a>
</div>
)}
<div className="p-4 bg-amber-50 border border-amber-200 rounded-xl mt-2">
<p className="text-xs font-bold text-amber-800 mb-1">⚠️ Ceny mają charakter orientacyjny</p>
<p className="text-xs text-amber-700 mb-3">
Podane ceny urządzeń są cenami brutto (23% VAT). Koszt montażu jest szacunkowy. Zachęcamy do darmowej wyceny.
</p>
<div className="flex flex-col sm:flex-row gap-2">
<a href={`tel:${companyDetails.phoneRaw}`}>
<button className="flex items-center gap-2 px-4 py-2 bg-accent text-white rounded-xl text-xs font-bold hover:bg-accent/90 transition-colors">
<Phone className="size-3.5" /> Bezpłatna wycena
</button>
</a>
<Link href="/kontakt" className="flex items-center gap-2 px-4 py-2 bg-amber-100 text-amber-800 rounded-xl text-xs font-bold hover:bg-amber-200 transition-colors border border-amber-300">
Prześlij zdjęcia
</Link>
</div>
</div>
</div>
)}
{/* Nawigacja */}
<div className="flex justify-between mt-8 pt-6 border-t border-border">
{step > 0 ? (
<button onClick={() => setStep(s => s - 1)}
className="flex items-center gap-2 px-4 py-2.5 border border-border rounded-xl text-sm font-semibold text-primary hover:border-accent transition-colors">
<ArrowLeft className="size-4" /> Wstecz
</button>
) : <div />}
{step < STEPS.length - 1 ? (
!(step === 0 && deviceType === 'pompa') && (
<button onClick={handleNext}
className="flex items-center gap-2 px-6 py-2.5 bg-accent text-white rounded-xl text-sm font-bold hover:bg-accent/90 transition-colors">
Dalej <ArrowRight className="size-4" />
</button>
)
) : (
<Link href="/sklep" className="flex items-center gap-2 px-6 py-2.5 bg-accent text-white rounded-xl text-sm font-bold hover:bg-accent/90 transition-colors">
Przejdź do sklepu <ArrowRight className="size-4" />
</Link>
)}
</div>
</div>
</div>
</main>
);
}