Files
thermcool/components/kalkulator/KalkulatorClient.tsx
T

466 lines
23 KiB
TypeScript
Raw 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, useMemo } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { ArrowRight, ArrowLeft, Check, Thermometer, Home, Zap, ShoppingCart, Phone, RotateCcw } from 'lucide-react';
import { Product } from '@/content/products';
import { companyDetails } from '@/content/company';
interface Props {
products: Product[];
}
// 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): { min: number; max: number } {
const base = rooms === 1 ? 1200 : rooms === 2 ? 2800 : rooms === 3 ? 4200 : 5500;
const pipeExtra = pipes === 'short' ? 0 : pipes === 'medium' ? 300 : pipes === 'long' ? 700 : 1200;
return { min: base + pipeExtra, max: base + pipeExtra + 600 };
}
const STEPS = ['Pomieszczenie', 'Konfiguracja', 'Instalacja', 'Budżet'] as const;
export function KalkulatorClient({ products }: Props) {
const [step, setStep] = useState(0);
const [building, setBuilding] = useState('apartment');
const [m2, setM2] = useState(25);
const [floor, setFloor] = useState('middle'); // ground | middle | top
const [sun, setSun] = useState('south'); // north | east | south | west
const [rooms, setRooms] = useState(1);
const [pipes, setPipes] = useState('medium'); // short(<3m) | medium(3-6m) | long(6-10m) | verylong(>10m)
const [budget, setBudget] = useState(8000);
const [showResults, setShowResults] = useState(false);
const requiredPower = useMemo(() => calculatePower(m2, floor, sun, building), [m2, floor, sun, building]);
const installCost = useMemo(() => estimateInstall(pipes, rooms), [pipes, rooms]);
const category = rooms === 1 ? 'scienne' : 'multisplit';
const results = useMemo(() => {
if (!showResults) return [];
const powerMin = requiredPower * 0.8;
const powerMax = requiredPower * 1.4;
const maxPrice = budget - installCost.min;
return products
.filter(p => {
if (p.category !== category) return false;
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);
})
.slice(0, 6);
}, [showResults, products, requiredPower, category, budget, installCost.min]);
const handleFinish = () => setShowResults(true);
const handleReset = () => { setShowResults(false); setStep(0); };
if (showResults) {
return (
<main className="bg-slate-50 min-h-screen py-12">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Podsumowanie */}
<div className="bg-white rounded-2xl border border-border p-6 mb-8">
<h1 className="text-2xl font-extrabold text-primary font-heading mb-4">
Dopasowane klimatyzatory
</h1>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{[
{ label: 'Wymagana moc', value: `${requiredPower} kW` },
{ label: 'Powierzchnia', value: `${m2} m²` },
{ label: 'Typ instalacji', value: rooms === 1 ? 'Split (1 pokój)' : `Multisplit (${rooms} pokoje)` },
{ label: 'Szac. montaż', value: `${installCost.min.toLocaleString('pl-PL')}–${installCost.max.toLocaleString('pl-PL')} zł` },
].map((s, i) => (
<div key={i} className="bg-slate-50 rounded-xl p-3">
<p className="text-xs text-text-muted mb-1">{s.label}</p>
<p className="text-base font-bold text-primary font-heading">{s.value}</p>
</div>
))}
</div>
</div>
{results.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
{results.map(product => (
<div key={product.slug} className="bg-white rounded-2xl border border-border overflow-hidden hover:border-accent hover:shadow-md transition-all">
<div className="relative aspect-[4/3] bg-slate-50">
<Image
src={product.image}
alt={product.name}
fill
className="object-contain p-6"
unoptimized={product.image.startsWith('http')}
/>
{product.specs?.['Klasa energetyczna'] && (
<span className="absolute top-2 left-2 px-2 py-0.5 bg-green-500 text-white text-xs font-bold rounded">
{product.specs['Klasa energetyczna'].split('/')[0]}
</span>
)}
</div>
<div className="p-4">
<p className="text-xs text-accent font-bold uppercase mb-1">{product.brand}</p>
<h3 className="text-sm font-bold text-primary font-heading leading-snug mb-3 line-clamp-2">
{product.name}
</h3>
<div className="flex flex-wrap gap-1.5 mb-3">
{product.specs?.['Moc chłodnicza'] && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded border border-blue-100">
{product.specs['Moc chłodnicza']} kW
</span>
)}
{product.specs?.['Hałas wewnątrz (dB)'] && (
<span className="px-2 py-0.5 bg-slate-50 text-text-muted text-xs rounded border border-border">
{product.specs['Hałas wewnątrz (dB)']} dB
</span>
)}
</div>
<div className="flex items-end justify-between">
<div>
<p className="text-lg font-extrabold text-primary font-heading leading-none">
{product.price?.toLocaleString('pl-PL')} zł
</p>
<p className="text-xs text-text-muted mt-0.5">+ montaż od {installCost.min.toLocaleString('pl-PL')} zł</p>
</div>
<Link href={`/sklep/${product.slug}`}>
<button className="flex items-center gap-1.5 px-3 py-2 bg-accent text-white rounded-xl text-xs font-bold hover:bg-accent/90 transition-colors">
Zobacz <ArrowRight className="size-3.5" />
</button>
</Link>
</div>
</div>
</div>
))}
</div>
) : (
<div className="bg-white rounded-2xl border border-border p-12 text-center">
<p className="text-text-muted mb-4">
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-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 className="mt-8 flex gap-4">
<button onClick={handleReset} className="flex items-center gap-2 px-5 py-3 bg-white border border-border rounded-xl text-sm font-semibold text-primary hover:border-accent transition-colors">
<RotateCcw className="size-4" /> Zacznij od nowa
</button>
<Link href="/sklep">
<button className="flex items-center gap-2 px-5 py-3 bg-white border border-border rounded-xl text-sm font-semibold text-primary hover:border-accent transition-colors">
Przeglądaj cały sklep <ArrowRight className="size-4" />
</button>
</Link>
</div>
</div>
</main>
);
}
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">
<h1 className="text-3xl font-extrabold text-primary font-heading mb-2">
Dobierz klimatyzator
</h1>
<p className="text-text-muted">Odpowiedz na 4 pytania — pokażemy Ci najlepsze opcje z naszej oferty</p>
</div>
{/* Pasek postępu */}
<div className="flex items-center gap-2 mb-8">
{STEPS.map((s, i) => (
<div key={s} className="flex-1 flex items-center gap-2">
<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 ${i === step ? 'text-accent' : 'text-text-muted'}`}>{s}</span>
{i < STEPS.length - 1 && <div className={`flex-1 h-0.5 ${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: Pomieszczenie */}
{step === 0 && (
<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>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">
Powierzchnia pomieszczenia: <span className="text-accent">{m2} m²</span>
</label>
<input
type="range" min={10} max={100} step={5} value={m2}
onChange={e => setM2(Number(e.target.value))}
className="w-full accent-accent"
/>
<div className="flex justify-between text-xs text-text-muted mt-1">
<span>10 m²</span><span>100 m²</span>
</div>
<div className="mt-3 p-3 bg-blue-50 rounded-xl text-sm text-blue-700 font-medium">
Wymagana moc: ok. <strong>{requiredPower} kW</strong>
<span className="text-xs font-normal ml-2">(dla {m2} m²)</span>
</div>
</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 1: Konfiguracja */}
{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-purple-50 flex items-center justify-center">
<Thermometer className="size-5 text-purple-600" />
</div>
<div>
<h2 className="text-lg font-bold text-primary font-heading">Ile pomieszczeń?</h2>
<p className="text-xs text-text-muted">1 pomieszczenie = split, więcej = multisplit</p>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
{[
{ value: 1, label: '1 pomieszczenie', sub: 'Klimatyzator split', icon: '🏠' },
{ value: 2, label: '2 pomieszczenia', sub: 'System multisplit 2+1', icon: '🏡' },
{ value: 3, label: '3 pomieszczenia', sub: 'System multisplit 3+1', icon: '🏘️' },
{ value: 4, label: '4-5 pomieszczeń', sub: 'System multisplit 4+1', icon: '🏗️' },
].map(o => (
<button key={o.value} onClick={() => setRooms(o.value)}
className={`p-4 rounded-xl border-2 text-left transition-all ${rooms === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<span className="text-2xl mb-2 block">{o.icon}</span>
<p className="text-sm font-bold text-primary">{o.label}</p>
<p className="text-xs text-text-muted">{o.sub}</p>
</button>
))}
</div>
<div className="p-3 bg-slate-50 rounded-xl text-xs text-text-muted">
{rooms === 1
? '✅ Klimatyzator split — jedna jednostka wewnętrzna + jedna zewnętrzna. Idealne do pojedynczego pomieszczenia.'
: `✅ System multisplit — jedna jednostka zewnętrzna obsługuje ${rooms} jednostki wewnętrzne. Ekonomiczne i estetyczne rozwiązanie dla całego domu.`}
</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 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>
)}
{/* 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>
<div className="flex justify-between items-center mb-3">
<label className="text-sm font-semibold text-primary">Budżet całkowity</label>
<span className="text-lg font-extrabold text-accent font-heading">{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="w-full accent-accent"
/>
<div className="flex justify-between text-xs text-text-muted mt-1">
<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. {Math.max(0, budget - installCost.min).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>{requiredPower} kW</strong> w cenie do <strong>{Math.max(0, budget - installCost.min).toLocaleString('pl-PL')} zł</strong>
</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 < 3 ? (
<button onClick={() => setStep(s => s + 1)}
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>
) : (
<button onClick={handleFinish}
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">
Pokaż dopasowane produkty <ArrowRight className="size-4" />
</button>
)}
</div>
</div>
</div>
</main>
);
}