Files
thermcool/components/shop/ProductPageClient.tsx
T

541 lines
26 KiB
TypeScript

'use client';
import { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import {
ArrowLeft, Phone, ShoppingCart, GitCompare, Check,
Thermometer, Volume2, Zap, Wind, Star, Info, Eye, X
} from 'lucide-react';
import { Product } from '@/content/products';
import { useCartStore } from '@/lib/cart-store';
import { useCompareStore } from '@/lib/compare-store';
import { getProductVariants } from '@/content/product-variants';
const brandLogos: Record<string, string> = {
'Gree': '/brands/gree.webp',
'Rotenso': '/brands/rotenso.webp',
'Toshiba': '/brands/toshiba.webp',
'Daikin': '/brands/daikin.webp',
'CHV': '/brands/chv.webp',
'Haier': '/brands/haier.webp',
'LG': '/brands/lg.webp',
'Samsung': '/brands/samsung.webp',
'Mitsubishi Electric': '/brands/mitsubishi.webp',
'Fuji Electric': '/brands/fuji.webp',
};
interface Props {
product: Product;
seriesProducts: Product[];
companyPhone: string;
companyPhoneRaw: string;
}
// Szacowany SEER na podstawie klasy energetycznej
function estimateSEER(klasa: string): number {
if (!klasa) return 5.0;
const k = klasa.toUpperCase();
if (k.includes('A+++')) return 8.5;
if (k.includes('A++')) return 6.1;
if (k.includes('A+')) return 4.6;
if (k.includes('A')) return 3.6;
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);
if (!nums) return 0;
if (nums.length === 1) return parseFloat(nums[0]);
// zakres: bierz środkową
const floats = nums.map(parseFloat);
return floats[Math.floor(floats.length / 2)];
}
const TABS = ['Opis', 'Specyfikacja', 'Montaż', 'Opinie'] as const;
type Tab = typeof TABS[number];
function seededViews(slug: string): number {
let hash = 0;
for (let i = 0; i < slug.length; i++) {
hash = (hash * 31 + slug.charCodeAt(i)) >>> 0;
}
return 18 + (hash % 50);
}
export function ProductPageClient({ product, seriesProducts, companyPhone, companyPhoneRaw }: Props) {
const [currentProduct, setCurrentProduct] = useState(product);
const [activeTab, setActiveTab] = useState<Tab>('Opis');
const [godziny, setGodziny] = useState(8);
const [cartState, setCartState] = useState<'idle' | 'added' | 'cart'>('idle');
const [showViewsBanner, setShowViewsBanner] = useState(true);
const viewsCount = seededViews(currentProduct.slug);
const variants = getProductVariants(currentProduct.slug);
const addToCart = useCartStore(s => s.addItem);
const { addItem: addCompare, removeItem: removeCompare, isAdded } = useCompareStore();
const inCompare = isAdded(currentProduct.slug);
const handleAddToCart = () => {
if (!currentProduct.price) return;
addToCart({
slug: currentProduct.slug,
name: currentProduct.name,
brand: currentProduct.brand,
image: currentProduct.image,
price: currentProduct.price,
});
setCartState('added');
setTimeout(() => setCartState('cart'), 1500);
};
const handleCompare = () => {
if (inCompare) removeCompare(currentProduct.slug);
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' },
currentProduct.specs['Moc grzewcza (kW)'] && { icon: Thermometer, label: 'Moc grzewcza', value: `${currentProduct.specs['Moc grzewcza (kW)']} kW`, color: 'text-orange-600 bg-orange-50' },
currentProduct.specs['Hałas wewnątrz (dB)'] && { icon: Volume2, label: 'Hałas wewnątrz', value: `${currentProduct.specs['Hałas wewnątrz (dB)']} dB`, color: 'text-purple-600 bg-purple-50' },
currentProduct.specs['Czynnik chłodniczy'] && { icon: Wind, label: 'Czynnik', value: currentProduct.specs['Czynnik chłodniczy'], color: 'text-teal-600 bg-teal-50' },
currentProduct.specs['Grzanie do'] && { icon: Zap, label: 'Grzanie do', value: `-${currentProduct.specs['Grzanie do']}°C`, color: 'text-accent bg-accent-light' },
].filter(Boolean) as { icon: any; label: string; value: string; color: string }[];
return (
<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>
<Link href="/sklep" className="hover:text-accent transition-colors">Sklep</Link>
<span>/</span>
<Link href={`/sklep?brand=${currentProduct.brand}`} className="hover:text-accent transition-colors">{currentProduct.brand}</Link>
<span>/</span>
<span className="text-primary font-medium truncate max-w-[200px]">{currentProduct.name}</span>
</nav>
<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
src={currentProduct.image}
alt={currentProduct.name}
fill
className="object-contain p-6"
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]}
</div>
)}
{currentProduct.category === 'multisplit' && (
<div className="absolute top-4 right-4 px-2.5 py-1 bg-accent text-white text-xs font-bold rounded-lg shadow">
MULTISPLIT
</div>
)}
</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] ? (
<div className="bg-white rounded-lg p-1.5 inline-block border border-border">
<Image
src={brandLogos[currentProduct.brand]}
alt={currentProduct.brand}
width={80}
height={28}
className="object-contain object-left"
unoptimized
/>
</div>
) : (
<span className="text-xs font-bold text-accent uppercase tracking-widest">
{currentProduct.brand}
</span>
)}
{currentProduct.brand === 'LG' && (
<Link href="/serwis-lg">
<div className="inline-flex items-center gap-1.5 px-3 py-1 bg-green-50 border border-green-200 rounded-full text-xs font-semibold text-green-700 hover:bg-green-100 transition-colors cursor-pointer">
<Check className="size-3.5" />
Serwisujemy urządzenia LG ✓
</div>
</Link>
)}
</div>
<h1 className="text-2xl md:text-3xl font-extrabold text-primary font-heading leading-tight">
{currentProduct.name}
</h1>
<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">
Dostępne moce (kW):
</p>
<div className="flex flex-wrap gap-2">
{variants.moce.map((v) => (
<button
key={v.slug}
onClick={() => {
const p = seriesProducts.find(sp => sp.slug === v.slug);
if (p) {
setCurrentProduct(p);
window.history.pushState(null, '', `/sklep/${v.slug}`);
}
}}
className={`px-3 py-1.5 rounded-xl text-xs font-bold border-2 transition-all ${
v.slug === currentProduct.slug
? 'border-accent bg-accent text-white shadow-sm'
: 'border-border bg-white text-primary hover:border-accent hover:text-accent'
}`}
>
{v.moc} kW
{v.slug !== currentProduct.slug && (
<span className="block text-[10px] font-normal opacity-70 mt-0.5">
{v.cena.toLocaleString('pl-PL')} zł
</span>
)}
{v.slug === currentProduct.slug && (
<span className="block text-[10px] font-normal opacity-80 mt-0.5">
{v.cena.toLocaleString('pl-PL')} zł
</span>
)}
</button>
))}
</div>
</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">
Wykończenie:
</p>
<div className="flex flex-wrap gap-2">
{variants.wykończenia.map((v) => (
<button
key={v.slug}
onClick={() => {
const p = seriesProducts.find(sp => sp.slug === v.slug);
if (p) {
setCurrentProduct(p);
window.history.pushState(null, '', `/sklep/${v.slug}`);
}
}}
className={`px-3 py-1.5 rounded-xl text-xs font-bold border-2 transition-all ${
v.slug === currentProduct.slug
? 'border-accent bg-accent text-white shadow-sm'
: 'border-border bg-white text-primary hover:border-accent hover:text-accent'
}`}
>
{v.wykończenie}
</button>
))}
</div>
</div>
)}
{/* Ikony parametrów */}
{params.length > 0 && (
<div className="flex flex-wrap gap-2">
{params.map((p, i) => {
const Icon = p.icon;
return (
<div key={i} className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg ${p.color} border border-current/10`}>
<Icon className="size-3.5 shrink-0" />
<div className="min-w-0">
<p className="text-[9px] font-medium opacity-70 leading-none mb-0.5">{p.label}</p>
<p className="text-[11px] font-bold truncate">{p.value}</p>
</div>
</div>
);
})}
</div>
)}
{/* Cena i CTA */}
<div className="bg-white rounded-2xl border border-border p-4">
{currentProduct.price !== null ? (
<>
<div className="flex items-end gap-3 mb-1">
<span className="text-2xl font-extrabold text-primary font-heading">
{currentProduct.price.toLocaleString('pl-PL')} zł
</span>
</div>
<p className="text-xs text-text-muted mb-1">cena urządzenia brutto (23% VAT)</p>
<p className="text-xs text-text-muted mb-4">Montaż wyceniamy indywidualnie — zadzwoń lub napisz</p>
<div className="flex flex-col sm:flex-row gap-3">
{cartState === 'idle' && (
<button
onClick={handleAddToCart}
className="flex-1 flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl font-bold text-sm bg-accent text-white hover:bg-accent/90 transition-all"
>
<ShoppingCart className="size-4" /> Do koszyka
</button>
)}
{cartState === 'added' && (
<button
disabled
className="flex-1 flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl font-bold text-sm bg-green-500 text-white"
>
<Check className="size-4" /> Dodano do koszyka
</button>
)}
{cartState === 'cart' && (
<Link href="/koszyk" className="flex-1 w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl font-bold text-sm bg-green-500 text-white hover:bg-green-600 transition-colors">
<ShoppingCart className="size-4" /> Zobacz koszyk
</Link>
)}
<Link href="/kontakt" className="flex-1 w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl font-bold text-sm border-2 border-primary text-primary hover:bg-primary hover:text-white transition-all">
Zapytaj o montaż
</Link>
</div>
<button
onClick={handleCompare}
className={`w-full mt-2 flex items-center justify-center gap-2 py-2 px-4 rounded-xl text-xs font-medium transition-colors ${
inCompare ? 'bg-accent/10 text-accent border border-accent' : 'bg-slate-50 text-text-muted border border-border hover:border-accent hover:text-accent'
}`}
>
<GitCompare className="size-3.5" />
{inCompare ? 'Usuń z porównania' : 'Dodaj do porównania'}
</button>
</>
) : (
<>
<p className="text-lg font-bold text-primary font-heading mb-1">Cena na zapytanie</p>
<p className="text-sm text-text-muted mb-4">Multisplit — cena zależy od konfiguracji jednostek wewnętrznych.</p>
<a href={`tel:${companyPhoneRaw}`} className="flex items-center justify-center gap-2 py-3 px-4 rounded-xl bg-accent text-white font-bold text-sm hover:bg-accent/90 transition-colors">
<Phone className="size-4" /> Zadzwoń: {companyPhone}
</a>
</>
)}
</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"
>
<span className="text-xs font-semibold">Profesjonalna porada</span>
<span className="flex items-center gap-2 font-bold">
<Phone className="size-4" /> {companyPhone}
</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">
<Zap className="size-4 text-accent" />
<h3 className="text-sm font-bold text-primary font-heading">Kalkulator kosztów energii</h3>
<div className="group relative ml-auto">
<Info className="size-4 text-text-muted cursor-help" />
<div className="absolute right-0 bottom-6 w-56 bg-primary text-white text-xs rounded-lg p-2.5 hidden group-hover:block z-10">
Szacunek przy cenie prądu {cenaKwh} zł/kWh. Rzeczywiste koszty mogą się różnić.
</div>
</div>
</div>
<div className="flex items-center gap-4 mb-4">
<label className="text-xs text-text-muted shrink-0">Godziny użycia dziennie:</label>
<input
type="range"
min={1}
max={24}
value={godziny}
onChange={e => setGodziny(Number(e.target.value))}
className="range range-xs range-accent flex-1"
/>
<span className="text-sm font-bold text-primary w-8 text-right">{godziny}h</span>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-xl p-3 text-center">
<p className="text-xs text-text-muted mb-1">Koszt / godzinę</p>
<p className="text-lg font-bold text-primary font-heading">
{kosztGodzina.toFixed(2)} zł
</p>
</div>
<div className="bg-accent-light rounded-xl p-3 text-center border border-accent/20">
<p className="text-xs text-accent mb-1">Koszt miesięczny</p>
<p className="text-lg font-bold text-accent font-heading">
{kosztMiesiac.toFixed(2)} zł
</p>
</div>
</div>
<p className="text-[10px] text-text-muted mt-2 text-center">
Szacunek przy {cenaKwh} zł/kWh · {godziny}h/dzień · 30 dni
{!currentProduct.specs['Pobór mocy chłodzenie (kW)'] && ' · pobór szacowany z klasy energetycznej'}
</p>
</div>
)}
</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 => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-5 py-3 text-sm font-semibold whitespace-nowrap transition-colors border-b-2 -mb-px ${
activeTab === tab
? 'border-accent text-accent'
: 'border-transparent text-text-muted hover:text-primary'
}`}
>
{tab}
</button>
))}
</div>
{/* Tab: Opis */}
{activeTab === 'Opis' && (
<div className="max-w-3xl">
{currentProduct.shortDescription ? (
<p className="text-text-muted leading-relaxed text-base">{currentProduct.shortDescription}</p>
) : (
<p className="text-text-muted">Brak opisu produktu.</p>
)}
</div>
)}
{/* Tab: Specyfikacja */}
{activeTab === 'Specyfikacja' && (
<div className="max-w-2xl">
{Object.keys(currentProduct.specs).length > 0 ? (
<div className="rounded-xl border border-border overflow-hidden">
<table className="w-full text-sm">
<tbody>
{Object.entries(currentProduct.specs).map(([key, val], idx) => (
<tr key={key} className={idx % 2 === 0 ? 'bg-white' : 'bg-slate-50'}>
<td className="px-5 py-3 text-text-muted font-medium w-1/2 border-r border-border">{key}</td>
<td className="px-5 py-3 text-primary font-semibold">{val}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-text-muted">Brak specyfikacji.</p>
)}
</div>
)}
{/* Tab: Montaż */}
{activeTab === 'Montaż' && (
<div className="max-w-2xl space-y-4">
<div className="bg-white rounded-xl border border-border p-5">
<h3 className="font-bold text-primary font-heading mb-3">Montaż klimatyzacji</h3>
<p className="text-sm text-text-muted leading-relaxed mb-4">
Oferujemy profesjonalny montaż wszystkich klimatyzatorów z naszego sklepu na terenie Dolnego Śląska.
Montaż wykonujemy z uprawnieniami F-Gaz, estetycznie i terminowo.
</p>
<ul className="space-y-2 text-sm text-text-muted">
{['Bezpłatna wycena montażu przed realizacją', 'Uprawnienia F-Gaz — wymóg prawny', 'Próba szczelności i próżnia po montażu', 'Szkolenie z obsługi urządzenia', 'Gwarancja na wykonaną instalację'].map(item => (
<li key={item} className="flex items-center gap-2">
<Check className="size-4 text-green-500 shrink-0" />
{item}
</li>
))}
</ul>
<a href={`tel:${companyPhoneRaw}`} className="mt-4 inline-flex items-center gap-2 btn btn-primary text-white font-bold text-sm">
<Phone className="size-4" /> Zapytaj o montaż
</a>
</div>
</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">
<h3 className="font-bold text-primary font-heading mb-4">Dodaj opinię</h3>
<form className="space-y-4" onSubmit={(e) => { e.preventDefault(); alert('Opinia została wysłana i oczekuje na moderację.'); e.currentTarget.reset(); }}>
<div className="flex gap-1 mb-2">
{[1,2,3,4,5].map(star => (
<Star key={star} className="size-6 text-amber-400 cursor-pointer fill-amber-400 hover:scale-110 transition-transform" />
))}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-sm font-semibold text-primary">Imię</label>
<input type="text" required className="w-full px-4 py-2.5 rounded-xl border border-border bg-white text-sm focus:outline-none focus:ring-2 focus:ring-accent/50" placeholder="Twoje imię" />
</div>
<div className="space-y-1.5">
<label className="text-sm font-semibold text-primary">Email</label>
<input type="email" required className="w-full px-4 py-2.5 rounded-xl border border-border bg-white text-sm focus:outline-none focus:ring-2 focus:ring-accent/50" placeholder="twó[email protected]" />
</div>
</div>
<div className="space-y-1.5">
<label className="text-sm font-semibold text-primary">Treść opinii</label>
<textarea required rows={4} className="w-full px-4 py-2.5 rounded-xl border border-border bg-white text-sm focus:outline-none focus:ring-2 focus:ring-accent/50" placeholder="Podziel się swoimi wrażeniami..."></textarea>
</div>
<button type="submit" className="btn btn-primary font-bold text-white w-full sm:w-auto">Wyślij opinię</button>
</form>
</div>
<div className="space-y-4">
<h3 className="font-bold text-primary font-heading">Opinie klientów (0)</h3>
<p className="text-sm text-text-muted">Bądź pierwszą osobą, która oceni ten produkt!</p>
</div>
</div>
)}
</div>
</div>
{showViewsBanner && (
<div className="fixed bottom-24 left-6 z-50 flex items-center gap-3 bg-white border border-amber-200 shadow-lg rounded-xl px-4 py-3 max-w-xs animate-in slide-in-from-left-4 duration-300">
<Eye className="size-4 text-amber-500 shrink-0" />
<p className="text-sm text-gray-800">
W ostatnich 30 dniach interesuje się tym produktem <span className="font-bold">{viewsCount}</span> osób.
</p>
<button
onClick={() => setShowViewsBanner(false)}
className="text-gray-400 hover:text-gray-600 transition-colors shrink-0 ml-1"
aria-label="Zamknij"
>
<X className="size-4" />
</button>
</div>
)}
</main>
);
}