feat: Zaktualizuj sekcję hero, nawigację, kalkulator i formularz zgłoszenia awarii

This commit is contained in:
MichalC
2026-06-28 15:03:36 +02:00
parent 002a325dfb
commit 59808f1243
22 changed files with 1465 additions and 345 deletions
+119 -91
View File
@@ -11,7 +11,6 @@ import { Product } from '@/content/products';
import { useCartStore } from '@/lib/cart-store';
import { useCompareStore } from '@/lib/compare-store';
import { getProductVariants } from '@/content/product-variants';
import { useRouter } from 'next/navigation';
const brandLogos: Record<string, string> = {
'Gree': '/brands/gree.webp',
@@ -29,6 +28,7 @@ const brandLogos: Record<string, string> = {
interface Props {
product: Product;
seriesProducts: Product[];
companyPhone: string;
companyPhoneRaw: string;
}
@@ -55,48 +55,54 @@ function parseMoc(val: string): number {
return floats[Math.floor(floats.length / 2)];
}
const TABS = ['Opis', 'Specyfikacja', 'Montaż', 'Dostawa'] as const;
const TABS = ['Opis', 'Specyfikacja', 'Montaż'] as const;
type Tab = typeof TABS[number];
export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Props) {
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 [added, setAdded] = useState(false);
const [cartState, setCartState] = useState<'idle' | 'added' | 'cart'>('idle');
const router = useRouter();
const variants = getProductVariants(product.slug);
const variants = getProductVariants(currentProduct.slug);
const addToCart = useCartStore(s => s.addItem);
const { addItem: addCompare, removeItem: removeCompare, isAdded } = useCompareStore();
const inCompare = isAdded(product.slug);
const inCompare = isAdded(currentProduct.slug);
const handleAddToCart = () => {
if (!product.price) return;
addToCart({ slug: product.slug, name: product.name, brand: product.brand, image: product.image, price: product.price });
setAdded(true);
setTimeout(() => setAdded(false), 1500);
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(product.slug);
else addCompare({ slug: product.slug, name: product.name, brand: product.brand, price: product.price, image: product.image, specs: product.specs });
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(product.specs['Moc chłodnicza'] || '');
const pobórMocy = parseMoc(product.specs['Pobór mocy chłodzenie (kW)'] || '') || (mocChlodnicza / estimateSEER(product.specs['Klasa energetyczna'] || ''));
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 = [
product.specs['Klasa energetyczna'] && { icon: Star, label: 'Klasa energetyczna', value: product.specs['Klasa energetyczna'], color: 'text-green-600 bg-green-50' },
product.specs['Moc chłodnicza'] && { icon: Thermometer, label: 'Moc chłodnicza', value: `${product.specs['Moc chłodnicza']} kW`, color: 'text-blue-600 bg-blue-50' },
product.specs['Moc grzewcza (kW)'] && { icon: Thermometer, label: 'Moc grzewcza', value: `${product.specs['Moc grzewcza (kW)']} kW`, color: 'text-orange-600 bg-orange-50' },
product.specs['Hałas wewnątrz (dB)'] && { icon: Volume2, label: 'Hałas wewnątrz', value: `${product.specs['Hałas wewnątrz (dB)']} dB`, color: 'text-purple-600 bg-purple-50' },
product.specs['Czynnik chłodniczy'] && { icon: Wind, label: 'Czynnik', value: product.specs['Czynnik chłodniczy'], color: 'text-teal-600 bg-teal-50' },
product.specs['Grzanie do'] && { icon: Zap, label: 'Grzanie do', value: `-${product.specs['Grzanie do']}°C`, color: 'text-accent bg-accent-light' },
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 (
@@ -109,31 +115,31 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
<span>/</span>
<Link href="/sklep" className="hover:text-accent transition-colors">Sklep</Link>
<span>/</span>
<Link href={`/sklep?brand=${product.brand}`} className="hover:text-accent transition-colors">{product.brand}</Link>
<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]">{product.name}</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-square bg-white rounded-2xl border border-border overflow-hidden">
<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={product.image}
alt={product.name}
src={currentProduct.image}
alt={currentProduct.name}
fill
className="object-contain p-8"
className="object-contain p-6"
priority
unoptimized={product.image.startsWith('http')}
unoptimized={currentProduct.image.startsWith('http')}
/>
{/* Badges */}
{product.specs['Klasa energetyczna'] && (
{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">
{product.specs['Klasa energetyczna'].split('/')[0]}
{currentProduct.specs['Klasa energetyczna'].split('/')[0]}
</div>
)}
{product.category === 'multisplit' && (
{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>
@@ -142,36 +148,45 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
</div>
{/* PRAWA: Info */}
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-3">
{/* Nagłówek */}
<div>
<div className="mb-2">
{brandLogos[product.brand] ? (
<div className="bg-white rounded-lg p-2 inline-block border border-border">
<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[product.brand]}
alt={product.brand}
width={120}
height={36}
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">
{product.brand}
{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">
{product.name}
{currentProduct.name}
</h1>
<p className="text-xs text-text-muted mt-1.5 mb-4">SKU: {product.sku}</p>
<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-4">
<div className="mb-3">
<p className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
Dostępne moce (kW):
</p>
@@ -179,20 +194,26 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{variants.moce.map((v) => (
<button
key={v.slug}
onClick={() => router.push(`/sklep/${v.slug}`)}
className={`px-4 py-2 rounded-xl text-sm font-bold border-2 transition-all ${
v.slug === product.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 !== product.slug && (
{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 === product.slug && (
{v.slug === currentProduct.slug && (
<span className="block text-[10px] font-normal opacity-80 mt-0.5">
{v.cena.toLocaleString('pl-PL')} zł
</span>
@@ -205,7 +226,7 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{/* Warianty wykończenia */}
{variants && variants.wykończenia.length > 0 && (
<div className="mb-4">
<div className="mb-3">
<p className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
Wykończenie:
</p>
@@ -213,9 +234,15 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{variants.wykończenia.map((v) => (
<button
key={v.slug}
onClick={() => router.push(`/sklep/${v.slug}`)}
className={`px-4 py-2 rounded-xl text-sm font-bold border-2 transition-all ${
v.slug === product.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'
}`}
@@ -233,11 +260,11 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{params.map((p, i) => {
const Icon = p.icon;
return (
<div key={i} className={`flex items-center gap-2 px-3 py-2 rounded-xl ${p.color} border border-current/10`}>
<Icon className="size-4 shrink-0" />
<div key={i} className={`flex items-center gap-2 px-2.5 py-1.5 rounded-xl ${p.color} border border-current/10`}>
<Icon className="size-3.5 shrink-0" />
<div className="min-w-0">
<p className="text-[10px] font-medium opacity-70 leading-none mb-0.5">{p.label}</p>
<p className="text-xs font-bold truncate">{p.value}</p>
<p className="text-[11px] font-bold truncate">{p.value}</p>
</div>
</div>
);
@@ -246,28 +273,43 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
)}
{/* Cena i CTA */}
<div className="bg-white rounded-2xl border border-border p-5">
{product.price !== null ? (
<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-3xl font-extrabold text-primary font-heading">
{product.price.toLocaleString('pl-PL')} zł
<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">
<button
onClick={handleAddToCart}
className={`flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-xl font-bold text-sm transition-all ${
added ? 'bg-green-500 text-white' : 'bg-accent text-white hover:bg-accent/90'
}`}
>
{added ? <><Check className="size-4" /> Dodano do koszyka</> : <><ShoppingCart className="size-4" /> Do koszyka</>}
</button>
{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">
<button className="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
</button>
</Link>
)}
<Link href="/kontakt" className="flex-1">
<button className="w-full flex items-center justify-center gap-2 py-3 px-4 rounded-xl font-bold text-sm border-2 border-primary text-primary hover:bg-primary hover:text-white transition-all">
<button className="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ż
</button>
</Link>
@@ -297,9 +339,9 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{/* Profesjonalna porada */}
<a
href={`tel:${companyPhoneRaw}`}
className="flex items-center justify-between px-5 py-3.5 bg-primary rounded-xl text-white hover:bg-primary/90 transition-colors"
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-sm font-semibold">Profesjonalna porada</span>
<span className="text-xs font-semibold">Profesjonalna porada</span>
<span className="flex items-center gap-2 font-bold">
<Phone className="size-4" /> {companyPhone}
</span>
@@ -307,7 +349,7 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{/* Kalkulator kosztów energii */}
{mocChlodnicza > 0 && (
<div className="bg-white rounded-2xl border border-border p-5">
<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>
@@ -348,7 +390,7 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
</div>
<p className="text-[10px] text-text-muted mt-2 text-center">
Szacunek przy {cenaKwh} zł/kWh · {godziny}h/dzień · 30 dni
{!product.specs['Pobór mocy chłodzenie (kW)'] && ' · pobór szacowany z klasy energetycznej'}
{!currentProduct.specs['Pobór mocy chłodzenie (kW)'] && ' · pobór szacowany z klasy energetycznej'}
</p>
</div>
)}
@@ -376,8 +418,8 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{/* Tab: Opis */}
{activeTab === 'Opis' && (
<div className="max-w-3xl">
{product.shortDescription ? (
<p className="text-text-muted leading-relaxed text-base">{product.shortDescription}</p>
{currentProduct.shortDescription ? (
<p className="text-text-muted leading-relaxed text-base">{currentProduct.shortDescription}</p>
) : (
<p className="text-text-muted">Brak opisu produktu.</p>
)}
@@ -387,11 +429,11 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{/* Tab: Specyfikacja */}
{activeTab === 'Specyfikacja' && (
<div className="max-w-2xl">
{Object.keys(product.specs).length > 0 ? (
{Object.keys(currentProduct.specs).length > 0 ? (
<div className="rounded-xl border border-border overflow-hidden">
<table className="w-full text-sm">
<tbody>
{Object.entries(product.specs).map(([key, val], idx) => (
{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>
@@ -430,21 +472,7 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
</div>
)}
{/* Tab: Dostawa */}
{activeTab === 'Dostawa' && (
<div className="max-w-2xl">
<div className="bg-white rounded-xl border border-border p-5">
<h3 className="font-bold text-primary font-heading mb-3">Informacje o dostawie</h3>
<p className="text-sm text-text-muted leading-relaxed">
Szczegóły dostawy ustalamy indywidualnie po złożeniu zamówienia. Skontaktujemy się z Tobą w ciągu 24 godzin
od otrzymania zamówienia, aby potwierdzić termin dostawy i montażu.
</p>
<Link href="/dostawa" className="mt-3 inline-block text-sm text-accent hover:underline font-medium">
Sprawdź politykę dostawy →
</Link>
</div>
</div>
)}
</div>
</div>
</main>