feat: Zaawansowane ulepszenia kalkulatora (paginacja, auto-zapis sesji, UI montazu)
This commit is contained in:
@@ -1,16 +1,43 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { ArrowRight, ArrowLeft, Check, Thermometer, Home, Zap, ShoppingCart, Phone, RotateCcw } from 'lucide-react';
|
import { ArrowRight, ArrowLeft, Check, Thermometer, Home, Zap, ShoppingCart, Phone, RotateCcw } from 'lucide-react';
|
||||||
import { Product } from '@/content/products';
|
import { Product } from '@/content/products';
|
||||||
import { companyDetails } from '@/content/company';
|
import { companyDetails } from '@/content/company';
|
||||||
|
import { ProductCard } from '@/components/shop/ProductCard';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
products: Product[];
|
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) {
|
||||||
|
setState(JSON.parse(item));
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}, [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 (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [state, setValue] as const;
|
||||||
|
}
|
||||||
|
|
||||||
// Oblicz wymaganą moc chłodniczą w kW
|
// Oblicz wymaganą moc chłodniczą w kW
|
||||||
function calculatePower(m2: number, floor: string, sun: string, building: string): number {
|
function calculatePower(m2: number, floor: string, sun: string, building: string): number {
|
||||||
let power = m2 * 0.1; // baza: 1 kW na 10 m²
|
let power = m2 * 0.1; // baza: 1 kW na 10 m²
|
||||||
@@ -32,30 +59,34 @@ function calculatePower(m2: number, floor: string, sun: string, building: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Szacunkowy koszt montażu
|
// Szacunkowy koszt montażu
|
||||||
function estimateInstall(pipes: string, rooms: number, installMethod: string): { min: number; max: number } {
|
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;
|
const base = rooms === 1 ? 1400 : rooms === 2 ? 2400 : rooms === 3 ? 3200 : 3800;
|
||||||
// Przybliżona liczba dodatkowych metrów (powyżej 3m)
|
// Przybliżona liczba dodatkowych metrów (powyżej 3m)
|
||||||
const extraMeters = pipes === 'short' ? 0 : pipes === 'medium' ? 3 : pipes === 'long' ? 7 : 15;
|
const extraMeters = pipes === 'short' ? 0 : pipes === 'medium' ? 3 : pipes === 'long' ? 7 : 15;
|
||||||
const methodPrice = installMethod === 'bruzda' ? 90 : 80; // zł za metr
|
const methodPrice = installMethod === 'bruzda' ? 90 : 80; // zł za metr
|
||||||
// Zostawmy bazowe koszty rur miedzianych (np. miedź, kable) i dodajmy koszt bruzdy/korytka
|
// 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;
|
const pipeBaseCost = pipes === 'short' ? 0 : pipes === 'medium' ? 200 : pipes === 'long' ? 500 : 1000;
|
||||||
const pipeExtra = pipeBaseCost + (extraMeters * methodPrice);
|
// 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 };
|
return { min: base + pipeExtra, max: base + pipeExtra + 600 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEPS = ['Typ', 'Pomieszczenie', 'Instalacja', 'Budżet'] as const;
|
const STEPS = ['Typ', 'Pomieszczenie', 'Instalacja', 'Budżet', 'Dobór'] as const;
|
||||||
|
|
||||||
export function KalkulatorClient({ products }: Props) {
|
export function KalkulatorClient({ products }: Props) {
|
||||||
const [deviceType, setDeviceType] = useState<'klimatyzacja' | 'pompa'>('klimatyzacja');
|
const [deviceType, setDeviceType] = useSessionState<'klimatyzacja' | 'pompa'>('calc_deviceType', 'klimatyzacja');
|
||||||
const [pumpFormSent, setPumpFormSent] = useState(false);
|
const [pumpFormSent, setPumpFormSent] = useState(false);
|
||||||
const [pumpForm, setPumpForm] = useState({ name: '', phone: '', city: '', message: '' });
|
const [pumpForm, setPumpForm] = useState({ name: '', phone: '', city: '', message: '' });
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useSessionState('calc_step', 0);
|
||||||
const [building, setBuilding] = useState('apartment');
|
const [building, setBuilding] = useSessionState('calc_building', 'apartment');
|
||||||
const [pomieszczenia, setPomieszczenia] = useState<{ nazwa: string; m2: number }[]>([
|
const [pomieszczenia, setPomieszczenia] = useSessionState<{ nazwa: string; m2: number }[]>('calc_pomieszczenia', [
|
||||||
{ nazwa: 'Pokój 1', m2: 20 }
|
{ nazwa: 'Pokój 1', m2: 20 }
|
||||||
]);
|
]);
|
||||||
const [floor, setFloor] = useState('middle'); // ground | middle | top
|
const [floor, setFloor] = useSessionState('calc_floor', 'middle'); // ground | middle | top
|
||||||
const [sun, setSun] = useState('south'); // north | east | south | west
|
const [sun, setSun] = useSessionState('calc_sun', 'south'); // north | east | south | west
|
||||||
|
|
||||||
const addRoom = () => {
|
const addRoom = () => {
|
||||||
if (pomieszczenia.length < 5) {
|
if (pomieszczenia.length < 5) {
|
||||||
@@ -72,11 +103,16 @@ export function KalkulatorClient({ products }: Props) {
|
|||||||
const updateRoom = (i: number, m2: number) => {
|
const updateRoom = (i: number, m2: number) => {
|
||||||
setPomieszczenia(prev => prev.map((r, idx) => idx === i ? { ...r, m2 } : r));
|
setPomieszczenia(prev => prev.map((r, idx) => idx === i ? { ...r, m2 } : r));
|
||||||
};
|
};
|
||||||
const [pipes, setPipes] = useState('medium'); // short(<3m) | medium(3-6m) | long(6-10m) | verylong(>10m)
|
const [pipes, setPipes] = useSessionState('calc_pipes', 'medium'); // short(<3m) | medium(3-6m) | long(6-10m) | verylong(>10m)
|
||||||
const [installMethod, setInstallMethod] = useState<'korytko' | 'bruzda'>('korytko');
|
const [installMethod, setInstallMethod] = useSessionState<'korytko' | 'bruzda'>('calc_installMethod', 'korytko');
|
||||||
const [budget, setBudget] = useState(8000);
|
const [outdoorUnitLocation, setOutdoorUnitLocation] = useSessionState<'balcony' | 'facade' | 'roof'>('calc_outdoorLocation', 'balcony');
|
||||||
const [showResults, setShowResults] = useState(false);
|
const [hasSocket, setHasSocket] = useSessionState<'yes' | 'no' | 'dont_know'>('calc_hasSocket', 'yes');
|
||||||
const [preferredBrand, setPreferredBrand] = useState('');
|
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 totalM2 = pomieszczenia.reduce((sum, p) => sum + p.m2, 0);
|
||||||
const rooms = pomieszczenia.length;
|
const rooms = pomieszczenia.length;
|
||||||
@@ -85,12 +121,12 @@ export function KalkulatorClient({ products }: Props) {
|
|||||||
[pomieszczenia, floor, sun, building]
|
[pomieszczenia, floor, sun, building]
|
||||||
);
|
);
|
||||||
const requiredPowerRounded = Math.round(requiredPower * 10) / 10;
|
const requiredPowerRounded = Math.round(requiredPower * 10) / 10;
|
||||||
const installCost = useMemo(() => estimateInstall(pipes, rooms, installMethod), [pipes, rooms, installMethod]);
|
const installCost = useMemo(() => estimateInstall(pipes, rooms, installMethod, outdoorUnitLocation, hasSocket), [pipes, rooms, installMethod, outdoorUnitLocation, hasSocket]);
|
||||||
|
|
||||||
const category = rooms === 1 ? 'scienne' : 'multisplit';
|
const category = rooms === 1 ? 'scienne' : 'multisplit';
|
||||||
|
|
||||||
const results = useMemo(() => {
|
const results = useMemo(() => {
|
||||||
if (!showResults || deviceType === 'pompa') return [];
|
if (step < 4 || deviceType === 'pompa') return [];
|
||||||
|
|
||||||
const maxPrice = budget * 0.75;
|
const maxPrice = budget * 0.75;
|
||||||
const powerMin = requiredPower * 0.6;
|
const powerMin = requiredPower * 0.6;
|
||||||
@@ -102,6 +138,27 @@ export function KalkulatorClient({ products }: Props) {
|
|||||||
|
|
||||||
if (preferredBrand && p.brand !== preferredBrand) 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 (category === 'multisplit') {
|
||||||
if (p.price !== null && p.price > maxPrice) return false;
|
if (p.price !== null && p.price > maxPrice) return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -119,18 +176,64 @@ export function KalkulatorClient({ products }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return filtered.length > 0
|
return filtered.length > 0
|
||||||
? filtered.slice(0, 6)
|
? filtered
|
||||||
: products
|
: products
|
||||||
.filter(p => p.category === category && p.price !== null)
|
.filter(p => p.category === category && p.price !== null)
|
||||||
.sort((a, b) => (a.price || 0) - (b.price || 0))
|
.sort((a, b) => (a.price || 0) - (b.price || 0));
|
||||||
.slice(0, 6);
|
}, [step, deviceType, products, requiredPower, category, budget, preferredBrand, energyClass, maxNoise, maxNoiseOut]);
|
||||||
}, [showResults, deviceType, products, requiredPower, category, budget, preferredBrand]);
|
|
||||||
|
|
||||||
const handleFinish = () => setShowResults(true);
|
const paginatedResults = useMemo(() => results.slice((page - 1) * 9, page * 9), [results, page]);
|
||||||
const handleReset = () => { setShowResults(false); setStep(0); };
|
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 handleFinish = () => {
|
||||||
|
setStep(4);
|
||||||
|
setPage(1);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setStep(0);
|
||||||
|
setPage(1);
|
||||||
|
Object.keys(window.sessionStorage).forEach(key => {
|
||||||
|
if (key.startsWith('calc_')) window.sessionStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
setDeviceType('klimatyzacja');
|
||||||
|
setBuilding('apartment');
|
||||||
|
setPomieszczenia([{ nazwa: 'Pokój 1', m2: 20 }]);
|
||||||
|
setFloor('middle');
|
||||||
|
setSun('south');
|
||||||
|
setPipes('medium');
|
||||||
|
setInstallMethod('korytko');
|
||||||
|
setOutdoorUnitLocation('balcony');
|
||||||
|
setHasSocket('yes');
|
||||||
|
setBudget(8000);
|
||||||
|
setPreferredBrand('');
|
||||||
|
setEnergyClass('');
|
||||||
|
setMaxNoise(null);
|
||||||
|
setMaxNoiseOut(null);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
setStep(s => s + 1);
|
setStep(s => s + 1);
|
||||||
|
setPage(1);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePumpSubmit = async () => {
|
const handlePumpSubmit = async () => {
|
||||||
@@ -139,180 +242,9 @@ export function KalkulatorClient({ products }: Props) {
|
|||||||
setPumpFormSent(true);
|
setPumpFormSent(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (showResults) {
|
|
||||||
if (deviceType === 'pompa') {
|
|
||||||
return (
|
|
||||||
<main className="bg-slate-50 min-h-screen py-12">
|
|
||||||
<div className="max-w-3xl mx-auto px-4 text-center">
|
|
||||||
<div className="bg-white rounded-2xl border border-border p-12">
|
|
||||||
<h1 className="text-3xl font-extrabold text-primary font-heading mb-4">
|
|
||||||
Wycena pompy ciepła
|
|
||||||
</h1>
|
|
||||||
<p className="text-text-muted mb-8 text-lg">
|
|
||||||
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-8 py-4 bg-accent text-white rounded-xl font-bold mx-auto hover:bg-accent/90 transition-colors text-lg">
|
|
||||||
<Phone className="size-5" /> {companyDetails.phone}
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
<div className="mt-8">
|
|
||||||
<button onClick={handleReset} className="flex items-center gap-2 px-5 py-3 border border-border rounded-xl text-sm font-semibold text-primary hover:border-accent transition-colors mx-auto">
|
|
||||||
<RotateCcw className="size-4" /> Zacznij od nowa
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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: `${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ł` },
|
|
||||||
].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 => {
|
|
||||||
const mocChlodnicza = parseFloat(
|
|
||||||
String(product.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]
|
|
||||||
);
|
|
||||||
const seer = mocChlodnicza > 0 ? mocChlodnicza / (mocChlodnicza * 0.5) : 5;
|
|
||||||
const pobor = mocChlodnicza / 5; // szacowany pobór przy SEER~5
|
|
||||||
const zuzycieMiesiac = (pobor * 8 * 30).toFixed(0); // 8h/dzień 30 dni
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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 flex flex-col h-full">
|
|
||||||
<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-1">
|
|
||||||
{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>
|
|
||||||
|
|
||||||
<p className="text-xs text-text-muted mt-1 mb-4">
|
|
||||||
~{zuzycieMiesiac} kWh/miesiąc (8h/dzień)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-end justify-between mt-auto">
|
|
||||||
<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 p-5 bg-amber-50 border border-amber-200 rounded-2xl">
|
|
||||||
<p className="text-sm font-bold text-amber-800 mb-2">⚠️ Ceny mają charakter orientacyjny</p>
|
|
||||||
<p className="text-sm text-amber-700 mb-4">
|
|
||||||
Podane ceny urządzeń są cenami brutto (23% VAT) i mogą się różnić
|
|
||||||
w zależności od aktualnej dostępności i kursu walut.
|
|
||||||
Koszt montażu jest szacunkowy — ostateczna wycena zależy od specyfiki instalacji.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-amber-700 mb-4">
|
|
||||||
Zachęcamy do skorzystania z <strong>bezpłatnej i niezobowiązującej wyceny</strong>.
|
|
||||||
Możesz też przesłać nam zdjęcia pomieszczeń — dobierzemy optymalne rozwiązanie.
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
|
||||||
<a href={`tel:${companyDetails.phoneRaw}`}>
|
|
||||||
<button className="flex items-center gap-2 px-5 py-2.5 bg-accent text-white rounded-xl text-sm font-bold hover:bg-accent/90 transition-colors">
|
|
||||||
<Phone className="size-4" /> Bezpłatna wycena
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
<Link href="/kontakt">
|
|
||||||
<button className="flex items-center gap-2 px-5 py-2.5 bg-amber-100 text-amber-800 rounded-xl text-sm font-bold hover:bg-amber-200 transition-colors border border-amber-300">
|
|
||||||
Prześlij zdjęcia pomieszczeń
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</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 (
|
return (
|
||||||
<main className="bg-slate-50 min-h-screen py-12">
|
<main className="bg-slate-50 min-h-screen py-12">
|
||||||
<div className="max-w-2xl mx-auto px-4 sm:px-6">
|
<div className={`mx-auto px-4 sm:px-6 transition-all duration-500 ${step === 4 && deviceType !== 'pompa' ? 'max-w-5xl' : 'max-w-2xl'}`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-3xl font-extrabold text-primary font-heading mb-2">
|
<h1 className="text-3xl font-extrabold text-primary font-heading mb-2">
|
||||||
@@ -629,6 +561,45 @@ export function KalkulatorClient({ products }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</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 any)}
|
||||||
|
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 any)}
|
||||||
|
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">
|
<div className="p-3 bg-orange-50 rounded-xl text-xs text-orange-700">
|
||||||
Szacowany koszt montażu dla Twojej konfiguracji:
|
Szacowany koszt montażu dla Twojej konfiguracji:
|
||||||
<strong className="ml-1">{installCost.min.toLocaleString('pl-PL')}–{installCost.max.toLocaleString('pl-PL')} zł</strong>
|
<strong className="ml-1">{installCost.min.toLocaleString('pl-PL')}–{installCost.max.toLocaleString('pl-PL')} zł</strong>
|
||||||
@@ -739,6 +710,189 @@ export function KalkulatorClient({ products }: Props) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
|
<button 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
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -759,10 +913,11 @@ export function KalkulatorClient({ products }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<button onClick={handleFinish}
|
<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">
|
<button 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" />
|
Przejdź do sklepu <ArrowRight className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user