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
+9 -3
View File
@@ -10,9 +10,10 @@ const resend = new Resend(process.env.RESEND_API_KEY);
const contactFormSchema = z.object({
name: z.string().min(2, "Imię musi mieć min. 2 znaki"),
phone: z.string().min(9, "Wprowadź poprawny numer telefonu"),
email: z.string().email("Niepoprawny adres e-mail"),
subject: z.string().min(1, "Wybierz temat"),
message: z.string().min(10, "Wiadomość musi mieć min. 10 znaków"),
email: z.union([z.literal(''), z.string().email("Niepoprawny adres e-mail")]).optional(),
city: z.string().min(1, "Wybierz miasto"),
subject: z.string().optional(),
message: z.string().optional(),
consent: z.boolean().refine((val) => val === true, "Zgoda RODO jest wymagana"),
});
@@ -23,6 +24,7 @@ export async function submitContactForm(prevState: any, formData: FormData) {
name: formData.get("name"),
phone: formData.get("phone"),
email: formData.get("email"),
city: formData.get("city"),
subject: formData.get("subject"),
message: formData.get("message"),
consent: formData.get("consent") === "true" || formData.get("consent") === "on",
@@ -57,6 +59,10 @@ export async function submitContactForm(prevState: any, formData: FormData) {
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>E-mail:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><a href="mailto:${validatedData.email}" style="color: #0F2A47; text-decoration: none;">${validatedData.email}</a></td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Miasto:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.city}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Temat:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.subject}</td>
+37
View File
@@ -7,6 +7,8 @@ import { Testimonials } from "@/components/sections/Testimonials";
import { BlogPreview } from "@/components/sections/BlogPreview";
import { CtaBanner } from "@/components/sections/CtaBanner";
import { companyDetails } from "@/content/company";
import { Wrench, Phone, ArrowRight } from "lucide-react";
import Link from "next/link";
export default function Home() {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
@@ -139,6 +141,41 @@ export default function Home() {
<WhyUs />
{/* ProjectsSection will go here in Group B */}
<Testimonials />
{/* Serwis LG */}
<section className="py-16 bg-white">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="bg-primary rounded-2xl p-8 md:p-10 flex flex-col md:flex-row items-start md:items-center gap-6">
<div className="flex-1">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-white/10 rounded-full text-white text-xs font-semibold mb-4">
<Wrench className="size-3.5" />
Specjalizacja
</div>
<h2 className="text-2xl md:text-3xl font-extrabold text-white font-heading mb-3">
Serwisujemy LG —<br />tam gdzie inni mówią nie
</h2>
<p className="text-white/75 text-sm leading-relaxed max-w-lg">
Większość firm odmawia naprawy klimatyzatorów LG. My mamy doświadczenie
i narzędzia żeby skutecznie diagnozować i naprawiać te urządzenia
na terenie całego Dolnego Śląska.
</p>
</div>
<div className="flex flex-col gap-3 shrink-0">
<Link href="/serwis-lg">
<button className="flex items-center gap-2 px-6 py-3 bg-accent text-white rounded-xl font-bold hover:bg-accent/90 transition-colors whitespace-nowrap">
Dowiedz się więcej <ArrowRight className="size-4" />
</button>
</Link>
<a href={`tel:${companyDetails.phoneRaw}`}>
<button className="flex items-center gap-2 px-6 py-3 bg-white/10 text-white rounded-xl font-bold hover:bg-white/20 transition-colors border border-white/20 whitespace-nowrap">
<Phone className="size-4" /> Zadzwoń teraz
</button>
</a>
</div>
</div>
</div>
</section>
<BlogPreview />
<CtaBanner bgVariant="accent-light" />
</>
+200
View File
@@ -0,0 +1,200 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { Check, Phone, Wrench, AlertTriangle, ArrowRight } from 'lucide-react';
import { companyDetails } from '@/content/company';
export const metadata: Metadata = {
title: 'Serwis klimatyzacji LG Wrocław — naprawa i przegląd | ThermCool',
description: 'Serwisujemy klimatyzatory LG na Dolnym Śląsku. Naprawa, przegląd, odgrzybianie — tam gdzie inne firmy odmawiają. Zadzwoń i umów wizytę.',
alternates: { canonical: '/serwis-lg' },
openGraph: {
title: 'Serwis klimatyzacji LG Wrocław | ThermCool',
description: 'Specjalizujemy się w serwisie klimatyzatorów LG. Naprawa, diagnostyka, przegląd na Dolnym Śląsku.',
},
};
const faqItems = [
{
q: 'Dlaczego inne firmy odmawiają serwisu klimatyzacji LG?',
a: 'Klimatyzatory LG wymagają specjalistycznej wiedzy i dedykowanych narzędzi diagnostycznych. Wiele firm unika tych urządzeń ze względu na złożoność układów elektronicznych i trudniejszy dostęp do części zamiennych. My posiadamy odpowiednie doświadczenie i narzędzia.',
},
{
q: 'Ile kosztuje przegląd klimatyzatora LG?',
a: 'Koszt przeglądu klimatyzatora LG zaczyna się od 250 zł i obejmuje czyszczenie filtrów, sprawdzenie ciśnienia czynnika chłodniczego, kontrolę układu elektrycznego i test działania. Dokładna wycena zależy od modelu i stanu urządzenia.',
},
{
q: 'Jak długo trwa naprawa klimatyzatora LG?',
a: 'Standardowy przegląd i czyszczenie to 1-2 godziny. Naprawa usterek zależy od rodzaju problemu — prostsze naprawy wykonujemy podczas jednej wizyty, przy bardziej skomplikowanych usterkach czas realizacji ustalamy indywidualnie po diagnostyce.',
},
{
q: 'Czy przyjeżdżacie poza Wrocław?',
a: 'Tak, obsługujemy cały Dolny Śląsk — Legnicę, Wałbrzych, Jelenią Górę, Lubin, Świdnicę i okoliczne miejscowości. Koszt dojazdu ustalamy indywidualnie.',
},
{
q: 'Czy naprawiacie wszystkie modele LG?',
a: 'Serwisujemy większość klimatyzatorów LG dostępnych na rynku, w tym modele z serii Artcool, Standard Plus, Dualcool i MultiSplit. W przypadku bardzo starych lub rzadkich modeli skontaktuj się z nami przed umówieniem wizyty.',
},
];
export default function SerwisLGPage() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Service',
name: 'Serwis klimatyzacji LG',
provider: {
'@type': 'HVACBusiness',
name: companyDetails.name,
telephone: companyDetails.phone,
address: {
'@type': 'PostalAddress',
addressLocality: companyDetails.address.city,
addressRegion: companyDetails.address.region,
addressCountry: 'PL',
},
},
areaServed: { '@type': 'AdministrativeArea', name: 'Dolny Śląsk' },
description: 'Serwis, naprawa i przegląd klimatyzatorów LG na Dolnym Śląsku.',
mainEntityOfPage: {
'@type': 'FAQPage',
mainEntity: faqItems.map(f => ({
'@type': 'Question',
name: f.q,
acceptedAnswer: { '@type': 'Answer', text: f.a },
})),
},
};
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<main>
{/* Hero */}
<section className="bg-primary text-white py-16 md:py-24">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-white/10 rounded-full text-sm font-medium mb-6">
<Wrench className="size-4" />
Specjalistyczny serwis LG
</div>
<h1 className="text-3xl md:text-5xl font-extrabold font-heading leading-tight mb-6">
Serwis klimatyzacji LG<br />
<span className="text-accent">tam gdzie inni mówią nie</span>
</h1>
<p className="text-lg text-white/80 mb-8 max-w-2xl">
Klimatyzatory LG mają opinię trudnych w serwisie — większość firm odmawia ich naprawy.
My mamy odpowiednie doświadczenie i narzędzia, żeby skutecznie diagnozować
i naprawiać te urządzenia na terenie Dolnego Śląska.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<a href={`tel:${companyDetails.phoneRaw}`}>
<button className="flex items-center gap-2 px-6 py-3 bg-accent text-white rounded-xl font-bold hover:bg-accent/90 transition-colors">
<Phone className="size-5" /> Zadzwoń: {companyDetails.phone}
</button>
</a>
<Link href="/kontakt">
<button className="flex items-center gap-2 px-6 py-3 bg-white/10 text-white rounded-xl font-bold hover:bg-white/20 transition-colors border border-white/20">
Napisz do nas <ArrowRight className="size-5" />
</button>
</Link>
</div>
</div>
</section>
{/* Dlaczego my */}
<section className="py-16 bg-white">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-start gap-4 p-5 bg-orange-50 border border-orange-200 rounded-2xl mb-10">
<AlertTriangle className="size-6 text-orange-500 shrink-0 mt-0.5" />
<div>
<p className="font-bold text-primary mb-1">Dlaczego inne firmy odmawiają serwisu LG?</p>
<p className="text-sm text-text-muted">
Klimatyzatory LG wymagają specjalistycznej wiedzy i dedykowanych narzędzi diagnostycznych.
Wiele firm serwisowych unika tych urządzeń ze względu na złożoność układów elektronicznych
i trudniejszy dostęp do oryginalnych części zamiennych. My podjęliśmy to wyzwanie
i dziś skutecznie naprawiamy to, od czego inni odchodzą.
</p>
</div>
</div>
<h2 className="text-2xl font-extrabold text-primary font-heading mb-8">Co robimy dla Twojego LG?</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[
{ title: 'Przegląd i czyszczenie', desc: 'Czyszczenie filtrów, lameli, tacy ociekowej. Sprawdzenie szczelności i ciśnienia czynnika R32/R410A.' },
{ title: 'Diagnostyka usterek', desc: 'Odczyt kodów błędów, pomiary elektryczne, identyfikacja uszkodzonych podzespołów.' },
{ title: 'Naprawa układu chłodniczego', desc: 'Uzupełnianie czynnika chłodniczego, naprawa nieszczelności, wymiana sprężarki.' },
{ title: 'Naprawa elektroniki', desc: 'Wymiana płyt głównych, modułów WiFi, czujników temperatury i sterowników.' },
{ title: 'Odgrzybianie i dezynfekcja', desc: 'Profesjonalne odgrzybianie klimatyzatora — eliminacja grzybów i bakterii z układu nawiewu.' },
{ title: 'Serwis gwarancyjny i pogwarancyjny', desc: 'Obsługujemy urządzenia zarówno w trakcie jak i po wygaśnięciu gwarancji producenta.' },
].map((item, i) => (
<div key={i} className="flex gap-3 p-4 bg-slate-50 rounded-xl">
<Check className="size-5 text-accent shrink-0 mt-0.5" />
<div>
<p className="font-bold text-primary text-sm">{item.title}</p>
<p className="text-xs text-text-muted mt-0.5">{item.desc}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* Obsługiwane serie LG */}
<section className="py-16 bg-slate-50">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-extrabold text-primary font-heading mb-6">
Obsługiwane serie klimatyzatorów LG
</h2>
<div className="flex flex-wrap gap-2">
{['LG Artcool', 'LG Standard Plus', 'LG Dualcool', 'LG MultiSplit', 'LG LGRED°', 'LG Inverter V', 'LG UV Nano', 'LG Deluxe'].map(s => (
<span key={s} className="px-4 py-2 bg-white border border-border rounded-xl text-sm font-semibold text-primary">
{s}
</span>
))}
</div>
<p className="text-xs text-text-muted mt-4">
Nie widzisz swojego modelu? Zadzwoń — prawdopodobnie też go obsługujemy.
</p>
</div>
</section>
{/* FAQ */}
<section className="py-16 bg-white">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-extrabold text-primary font-heading mb-8">
Często zadawane pytania
</h2>
<div className="flex flex-col gap-4">
{faqItems.map((item, i) => (
<details key={i} className="group bg-slate-50 rounded-xl border border-border overflow-hidden">
<summary className="flex items-center justify-between p-5 cursor-pointer font-semibold text-primary text-sm list-none">
{item.q}
<ArrowRight className="size-4 text-text-muted group-open:rotate-90 transition-transform shrink-0 ml-3" />
</summary>
<div className="px-5 pb-5 text-sm text-text-muted leading-relaxed border-t border-border pt-4">
{item.a}
</div>
</details>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16 bg-primary text-white">
<div className="max-w-2xl mx-auto px-4 text-center">
<h2 className="text-2xl font-extrabold font-heading mb-4">
Masz problem z klimatyzatorem LG?
</h2>
<p className="text-white/80 mb-8">
Zadzwoń lub napisz — odpowiadamy szybko i przyjeżdżamy na Dolny Śląsk.
</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 text-lg hover:bg-accent/90 transition-colors mx-auto">
<Phone className="size-5" /> {companyDetails.phone}
</button>
</a>
</div>
</section>
</main>
</>
);
}
+2
View File
@@ -21,6 +21,8 @@ export default function sitemap(): MetadataRoute.Sitemap {
'/regulamin',
'/polityka-prywatnosci',
'/reklamacje',
'/serwis-lg',
'/zgloszenie-awarii',
'/dostawa',
'/gwarancja',
].map((route) => ({
+14 -1
View File
@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation';
import { getProductBySlug, getProducts } from '@/content/products';
import { companyDetails } from '@/content/company';
import { ProductPageClient } from '@/components/shop/ProductPageClient';
import { getProductVariants } from '@/content/product-variants';
interface Props {
params: Promise<{ slug: string }>;
@@ -28,6 +29,13 @@ export default async function ProductPage({ params }: Props) {
const product = getProductBySlug(slug);
if (!product) notFound();
// Pobierz wszystkie produkty serii dla wariantów
const variants = getProductVariants(slug);
const allProducts = getProducts();
const seriesProducts = variants
? allProducts.filter(p => getProductVariants(p.slug)?.series === variants.series)
: [product];
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
const productJsonLd = product.price ? {
@@ -54,7 +62,12 @@ export default async function ProductPage({ params }: Props) {
{productJsonLd && (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }} />
)}
<ProductPageClient product={product} companyPhone={companyDetails.phone} companyPhoneRaw={companyDetails.phoneRaw} />
<ProductPageClient
product={product}
seriesProducts={seriesProducts}
companyPhone={companyDetails.phone}
companyPhoneRaw={companyDetails.phoneRaw}
/>
</>
);
}
+2 -2
View File
@@ -23,8 +23,8 @@ export default function ShopPage() {
subtitle={`${splitCount} klimatyzatorów split i ${multiCount} jednostek multisplit. Gree, Rotenso, Haier, Daikin, LG, Samsung i inne marki. Z montażem lub bez.`}
ctaText="Zapytaj o montaż"
ctaLink="/kontakt"
secondaryCtaText={undefined}
secondaryCtaLink={undefined}
secondaryCtaText=""
secondaryCtaLink=""
backgroundImage="https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=1600&q=80"
/>
<ProductsGrid products={products} brands={brands} />
+59
View File
@@ -0,0 +1,59 @@
'use server';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function submitAwariaForm(data: {
isUrgent: boolean;
brand: string;
model: string;
errorCode: string;
problemType: string;
description: string;
warranty: string;
lastService: string;
name: string;
phone: string;
city: string;
preferredDate: string;
preferredTime: string;
}) {
try {
await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL || '[email protected]',
to: process.env.RESEND_TO_EMAIL || '[email protected]',
subject: `${data.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe: ${data.brand} — ${data.city}`,
html: `
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;">
<div style="background:${data.isUrgent ? '#ef4444' : '#0F2A47'};padding:20px;text-align:center;">
<h1 style="color:#fff;margin:0;font-size:20px;">
${data.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe ThermCool
</h1>
</div>
<div style="padding:24px;">
<h2 style="font-size:16px;margin-top:0;">Dane klienta</h2>
<p><strong>Imię:</strong> ${data.name}</p>
<p><strong>Telefon:</strong> <a href="tel:${data.phone}">${data.phone}</a></p>
<p><strong>Miasto:</strong> ${data.city}</p>
${data.preferredDate ? `<p><strong>Preferowany termin:</strong> ${data.preferredDate} ${data.preferredTime || ''}</p>` : ''}
<h2 style="font-size:16px;">Urządzenie</h2>
<p><strong>Marka:</strong> ${data.brand}</p>
${data.model ? `<p><strong>Model:</strong> ${data.model}</p>` : ''}
${data.warranty ? `<p><strong>Gwarancja:</strong> ${data.warranty}</p>` : ''}
${data.lastService ? `<p><strong>Ostatni serwis:</strong> ${data.lastService}</p>` : ''}
<h2 style="font-size:16px;">Problem</h2>
<p><strong>Typ problemu:</strong> ${data.problemType}</p>
${data.errorCode ? `<p><strong>Kod błędu:</strong> ${data.errorCode}</p>` : ''}
${data.description ? `<p><strong>Opis:</strong> ${data.description}</p>` : ''}
</div>
</div>
`,
});
return { success: true };
} catch {
return { success: false };
}
}
+12
View File
@@ -0,0 +1,12 @@
import type { Metadata } from 'next';
import { AwariaClient } from '@/components/awaria/AwariaClient';
export const metadata: Metadata = {
title: 'Zgłoś awarię klimatyzacji | Serwis ThermCool Wrocław',
description: 'Zgłoś awarię klimatyzatora online. Szybka diagnostyka, kod błędu, preferowany termin wizyty. Obsługujemy Wrocław i cały Dolny Śląsk.',
alternates: { canonical: '/zgloszenie-awarii' },
};
export default function ZgloszenieAwariiPage() {
return <AwariaClient />;
}
+439
View File
@@ -0,0 +1,439 @@
'use client';
import { useState } from 'react';
import { AlertTriangle, Wrench, Phone, Check, Clock, Zap } from 'lucide-react';
import Link from 'next/link';
import { companyDetails } from '@/content/company';
import { submitAwariaForm } from '@/app/zgloszenie-awarii/actions';
// Kody błędów popularnych marek
const ERROR_CODES: Record<string, Record<string, string>> = {
'Gree': {
'E1': 'Błąd czujnika temperatury powietrza wewnętrznego',
'E2': 'Błąd czujnika temperatury wymiennika wewnętrznego',
'E3': 'Błąd czujnika temperatury zewnętrznej',
'E4': 'Wysokie ciśnienie — możliwy problem ze sprężarką lub skraplaczem',
'E5': 'Niskie ciśnienie — możliwy brak czynnika chłodniczego',
'E6': 'Błąd komunikacji między jednostkami',
'F1': 'Awaria czujnika temperatury — wymagana wymiana czujnika',
'H6': 'Blokada silnika wentylatora wewnętrznego',
},
'LG': {
'CH01': 'Błąd czujnika temperatury powietrza wewnętrznego',
'CH02': 'Błąd czujnika wymiennika wewnętrznego',
'CH03': 'Błąd czujnika wymiennika zewnętrznego',
'CH05': 'Błąd komunikacji — sprawdź okablowanie',
'CH07': 'Awaria wentylatora zewnętrznego',
'CH10': 'Błąd czujnika prądu sprężarki',
'CH38': 'Błąd modułu WiFi',
'C1': 'Problem z drenażem — sprawdź odpływ skroplin',
},
'Daikin': {
'A1': 'Awaria PCB — płyta główna jednostki wewnętrznej',
'A5': 'Przegrzanie — wymagane czyszczenie lub sprawdzenie czynnika',
'C4': 'Błąd czujnika wymiennika wewnętrznego',
'E1': 'Awaria PCB jednostki zewnętrznej',
'E3': 'Wyłącznik wysokiego ciśnienia',
'E4': 'Wyłącznik niskiego ciśnienia',
'F3': 'Niskie ciśnienie oleju sprężarki',
'U4': 'Błąd komunikacji wewnętrzna-zewnętrzna',
},
'Samsung': {
'E1': 'Błąd czujnika temperatury powietrza',
'E2': 'Błąd czujnika wymiennika ciepła',
'E3': 'Błąd silnika wentylatora',
'E4': 'Wyłącznik wysokiego ciśnienia',
'E6': 'Błąd komunikacji',
'F1': 'Awaria czujnika — wymagana diagnostyka',
'C4': 'Błąd czujnika temperatury zewnętrznej',
},
'Haier': {
'E1': 'Błąd czujnika temperatury wewnętrznej',
'E2': 'Błąd czujnika wymiennika wewnętrznego',
'E3': 'Błąd czujnika temperatury zewnętrznej',
'E4': 'Wyłącznik wysokiego ciśnienia',
'E5': 'Wyłącznik niskiego ciśnienia — sprawdź poziom czynnika',
'E6': 'Błąd komunikacji',
'F1': 'Awaria czujnika — wymiana',
},
'Rotenso': {
'E1': 'Błąd czujnika temperatury wewnętrznej',
'E2': 'Błąd wymiennika wewnętrznego',
'E3': 'Błąd czujnika zewnętrznego',
'E5': 'Niskie ciśnienie — sprawdź czynnik chłodniczy',
'E6': 'Błąd komunikacji między jednostkami',
'P1': 'Ochrona przed zamrożeniem',
'P4': 'Wyłącznik wysokiego ciśnienia',
},
'Mitsubishi Electric': {
'E1': 'Błąd czujnika wewnętrznego',
'E2': 'Błąd czujnika wymiennika',
'E4': 'Błąd czujnika zewnętrznego',
'E6': 'Problem z wentylatorem zewnętrznym',
'P1': 'Błąd czujnika powietrza zewnętrznego',
'P4': 'Wyłącznik wysokiego ciśnienia',
'P8': 'Przegrzanie sprężarki',
'U1': 'Błąd napięcia zasilania',
},
'Toshiba': {
'E01': 'Błąd czujnika temperatury wewnętrznej',
'E02': 'Błąd czujnika wymiennika wewnętrznego',
'E03': 'Błąd czujnika zewnętrznego',
'E04': 'Wyłącznik wysokiego ciśnienia',
'E07': 'Błąd komunikacji',
'E08': 'Awaria wentylatora zewnętrznego',
},
};
const BRANDS = ['Gree', 'Rotenso', 'Haier', 'LG', 'Samsung', 'Daikin', 'Mitsubishi Electric', 'Toshiba', 'Aux', 'Fuji Electric', 'Inna marka'];
const PROBLEM_TYPES = [
'Nie chłodzi / nie grzeje',
'Wyświetla kod błędu',
'Głośna praca / wibracje',
'Wyciek wody',
'Brzydki zapach',
'Nie włącza się',
'Pilot nie działa',
'Zamarza wymiennik',
'Inne',
];
export function AwariaClient() {
const [step, setStep] = useState<'form' | 'success'>('form');
const [isUrgent, setIsUrgent] = useState(false);
const [brand, setBrand] = useState('');
const [model, setModel] = useState('');
const [errorCode, setErrorCode] = useState('');
const [problemType, setProblemType] = useState('');
const [description, setDescription] = useState('');
const [lastService, setLastService] = useState('');
const [warranty, setWarranty] = useState('');
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [city, setCity] = useState('');
const [preferredDate, setPreferredDate] = useState('');
const [preferredTime, setPreferredTime] = useState('');
const [consent, setConsent] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// Podpowiedź kodu błędu
const errorHint = brand && errorCode && ERROR_CODES[brand]?.[errorCode.toUpperCase()];
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!consent) {
alert('Proszę zaznaczyć zgodę na przetwarzanie danych.');
return;
}
setIsSubmitting(true);
await submitAwariaForm({
isUrgent,
brand,
model,
errorCode,
problemType,
description,
warranty,
lastService,
name,
phone,
city,
preferredDate,
preferredTime,
});
setIsSubmitting(false);
setStep('success');
};
if (step === 'success') {
return (
<main className="bg-slate-50 min-h-screen flex items-center justify-center py-16">
<div className="max-w-md mx-auto px-4 text-center">
<div className="size-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
<Check className="size-10 text-green-600" />
</div>
<h1 className="text-2xl font-extrabold text-primary font-heading mb-3">
Zgłoszenie przyjęte!
</h1>
<p className="text-text-muted mb-2">
{isUrgent
? 'Twoje zgłoszenie oznaczono jako PILNE — skontaktujemy się z Tobą w ciągu 2 godzin.'
: 'Skontaktujemy się z Tobą w ciągu 24 godzin, aby ustalić termin wizyty serwisanta.'}
</p>
<p className="text-sm text-text-muted mb-8">
Jeśli sprawa jest pilna — zadzwoń bezpośrednio:
</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 mb-4">
<Phone className="size-4" /> {companyDetails.phone}
</button>
</a>
<Link href="/">
<button className="text-sm text-text-muted hover:text-accent transition-colors">
Wróć na stronę główną
</button>
</Link>
</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">
<div className="size-14 bg-red-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Wrench className="size-7 text-red-600" />
</div>
<h1 className="text-3xl font-extrabold text-primary font-heading mb-2">
Zgłoś awarię / serwis
</h1>
<p className="text-text-muted">Wypełnij formularz — oddzwonimy i ustalimy termin wizyty</p>
</div>
{/* Pilne zgłoszenie toggle */}
<div
onClick={() => setIsUrgent(!isUrgent)}
className={`flex items-center gap-4 p-4 rounded-2xl border-2 cursor-pointer mb-6 transition-all ${
isUrgent ? 'border-red-400 bg-red-50' : 'border-border bg-white hover:border-red-300'
}`}
>
<div className={`size-10 rounded-xl flex items-center justify-center shrink-0 ${isUrgent ? 'bg-red-500' : 'bg-slate-100'}`}>
<AlertTriangle className={`size-5 ${isUrgent ? 'text-white' : 'text-text-muted'}`} />
</div>
<div className="flex-1">
<p className={`font-bold text-sm ${isUrgent ? 'text-red-700' : 'text-primary'}`}>
{isUrgent ? '🚨 ZGŁOSZENIE PILNE — kontakt w ciągu 2 godzin' : 'Oznacz jako PILNE'}
</p>
<p className="text-xs text-text-muted">Np. upał, brak chłodzenia, awaria krytyczna</p>
</div>
<div className={`size-6 rounded-full border-2 flex items-center justify-center shrink-0 ${isUrgent ? 'border-red-500 bg-red-500' : 'border-slate-300'}`}>
{isUrgent && <Check className="size-3.5 text-white" />}
</div>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
{/* Sekcja 1: Urządzenie */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<Zap className="size-4 text-accent" /> Urządzenie
</h2>
<div className="flex flex-col gap-4">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Marka *</label>
<select
required value={brand} onChange={e => { setBrand(e.target.value); setErrorCode(''); }}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
>
<option value="">Wybierz markę...</option>
{BRANDS.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Model (opcjonalnie)</label>
<input
value={model} onChange={e => setModel(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
placeholder="Np. Gree Pular, LG Artcool 3,5 kW..."
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Na gwarancji?</label>
<select
value={warranty} onChange={e => setWarranty(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
>
<option value="">Nie wiem</option>
<option value="tak">Tak</option>
<option value="nie">Nie</option>
</select>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Ostatni serwis</label>
<select
value={lastService} onChange={e => setLastService(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
>
<option value="">Nie pamiętam</option>
<option value="<1rok">Mniej niż rok temu</option>
<option value="1-2lata">1-2 lata temu</option>
<option value="2-5lat">2-5 lat temu</option>
<option value=">5lat">Ponad 5 lat temu</option>
<option value="nigdy">Nigdy nie był serwisowany</option>
</select>
</div>
</div>
</div>
</div>
{/* Sekcja 2: Problem */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<AlertTriangle className="size-4 text-accent" /> Opis problemu
</h2>
<div className="flex flex-col gap-4">
<div>
<label className="text-xs font-medium text-primary mb-2 block">Rodzaj problemu *</label>
<div className="flex flex-wrap gap-2">
{PROBLEM_TYPES.map(p => (
<button
key={p} type="button"
onClick={() => setProblemType(p)}
className={`px-3 py-1.5 rounded-xl text-xs font-semibold border-2 transition-all ${
problemType === p ? 'border-accent bg-accent text-white' : 'border-border bg-white text-primary hover:border-accent'
}`}
>
{p}
</button>
))}
</div>
</div>
{/* Kod błędu z podpowiedzią */}
<div>
<label className="text-xs font-medium text-primary mb-1 block">
Kod błędu z wyświetlacza (opcjonalnie)
</label>
<input
value={errorCode}
onChange={e => setErrorCode(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
placeholder="Np. E1, F3, CH05..."
/>
{errorHint && (
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-xl text-xs text-blue-700">
<span className="font-bold">Kod {errorCode.toUpperCase()}:</span> {errorHint}
</div>
)}
{errorCode && !errorHint && brand && ERROR_CODES[brand] && (
<p className="mt-1 text-xs text-text-muted">Kodu nie rozpoznano — serwisant zdiagnozuje na miejscu.</p>
)}
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Szczegółowy opis</label>
<textarea
value={description} onChange={e => setDescription(e.target.value)}
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="Opisz kiedy się pojawił problem, jak często występuje, czy jest jakiś charakterystyczny dźwięk..."
/>
</div>
</div>
</div>
{/* Sekcja 3: Termin */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<Clock className="size-4 text-accent" /> Preferowany termin
</h2>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Data</label>
<input
type="date" value={preferredDate} onChange={e => setPreferredDate(e.target.value)}
min={new Date().toISOString().split('T')[0]}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
/>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Godzina</label>
<select
value={preferredTime} onChange={e => setPreferredTime(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
>
<option value="">Dowolna</option>
<option value="8:00-10:00">8:00 – 10:00</option>
<option value="10:00-12:00">10:00 – 12:00</option>
<option value="12:00-14:00">12:00 – 14:00</option>
<option value="14:00-16:00">14:00 – 16:00</option>
<option value="16:00-18:00">16:00 – 18:00</option>
</select>
</div>
</div>
</div>
{/* Sekcja 4: Dane kontaktowe */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<Phone className="size-4 text-accent" /> Dane kontaktowe
</h2>
<div className="flex flex-col gap-4">
<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
required value={name} onChange={e => setName(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
placeholder="Jan Kowalski"
/>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Telefon *</label>
<input
required type="tel" value={phone} onChange={e => setPhone(e.target.value)}
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"
/>
</div>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Miasto *</label>
<select
required value={city} onChange={e => setCity(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
>
<option value="">Wybierz miasto...</option>
{['Wrocław', 'Legnica', 'Wałbrzych', 'Jelenia Góra', 'Lubin', 'Głogów', 'Świdnica', 'Dzierżoniów', 'Oleśnica', 'Oława', 'Trzebnica', 'Środa Śląska', 'Siechnice', 'Inne'].map(c => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
</div>
</div>
{/* Zgoda RODO */}
<div
className="flex items-start gap-3 cursor-pointer"
onClick={() => setConsent(!consent)}
>
<div className={`size-5 rounded border-2 shrink-0 mt-0.5 flex items-center justify-center transition-colors ${
consent ? 'bg-accent border-accent' : 'bg-white border-slate-300'
}`}>
{consent && <Check className="size-3 text-white" />}
</div>
<span className="text-xs text-text-muted leading-relaxed">
Wyrażam zgodę na przetwarzanie danych osobowych podanych w formularzu
w celu obsługi zgłoszenia serwisowego przez firmę ThermCool. *
</span>
</div>
{/* Submit */}
<button
type="submit"
disabled={isSubmitting || !brand || !problemType || !name || !phone || !city || !consent}
className={`flex items-center justify-center gap-2 py-4 px-6 rounded-2xl font-bold text-base transition-all ${
isUrgent
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-accent hover:bg-accent/90 text-white'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
{isSubmitting ? 'Wysyłanie...' : isUrgent ? '🚨 Wyślij PILNE zgłoszenie' : 'Wyślij zgłoszenie serwisowe'}
</button>
<p className="text-xs text-text-muted text-center">
{isUrgent ? 'Odpowiadamy na pilne zgłoszenia w ciągu 2 godzin w godzinach pracy.' : 'Skontaktujemy się w ciągu 24 godzin.'}
{' '}Możesz też zadzwonić bezpośrednio: <a href={`tel:${companyDetails.phoneRaw}`} className="text-accent font-semibold">{companyDetails.phone}</a>
</p>
</form>
</div>
</main>
);
}
+74 -37
View File
@@ -4,27 +4,27 @@ import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
import { Loader2, CheckCircle2, AlertCircle, Check } from 'lucide-react';
import { submitContactForm } from '@/app/kontakt/actions';
import { companyDetails } from "@/content/company";
const contactFormSchema = z.object({
name: z.string().min(2, "Imię musi mieć min. 2 znaki"),
phone: z.string().min(9, "Wprowadź poprawny numer telefonu"),
email: z.string().email("Niepoprawny adres e-mail"),
subject: z.string().min(1, "Wybierz temat"),
message: z.string().min(10, "Wiadomość musi mieć min. 10 znaków"),
consent: z.boolean().refine((val) => val === true, "Zgoda RODO jest wymagana"),
email: z.union([z.literal(''), z.string().email("Niepoprawny adres e-mail")]).optional(),
city: z.string().min(1, "Wybierz miasto"),
subject: z.string().optional(),
message: z.string().optional(),
});
type ContactFormValues = z.infer<typeof contactFormSchema>;
export function ContactForm() {
const [serverState, setServerState] = useState<{ success?: boolean; message?: string } | null>(null);
const [consent, setConsent] = useState(false);
const {
register,
handleSubmit,
handleSubmit: hookFormSubmit,
formState: { errors, isSubmitting },
reset
} = useForm<ContactFormValues>({
@@ -33,20 +33,20 @@ export function ContactForm() {
name: '',
phone: '',
email: '',
city: '',
subject: '',
message: '',
consent: false,
}
});
const onSubmit = async (data: ContactFormValues) => {
setServerState(null);
// Przekształcamy dane do formatu FormData dla Server Action
const formData = new FormData();
Object.entries(data).forEach(([key, value]) => {
formData.append(key, value.toString());
if (value !== undefined) formData.append(key, value.toString());
});
formData.append("consent", consent ? 'true' : 'false');
const response = await submitContactForm(null, formData);
@@ -57,9 +57,19 @@ export function ContactForm() {
if (response.success) {
reset();
setConsent(false);
}
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
if (!consent) {
e.preventDefault();
alert('Proszę zaznaczyć zgodę na przetwarzanie danych.');
return;
}
hookFormSubmit(onSubmit)(e);
};
if (serverState?.success) {
return (
<div className="bg-green-50 text-green-800 p-8 rounded-2xl border border-green-200 flex flex-col items-center text-center">
@@ -77,7 +87,7 @@ export function ContactForm() {
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 bg-white p-6 md:p-8 rounded-2xl border border-border shadow-sm">
<form onSubmit={handleSubmit} className="space-y-6 bg-white p-6 md:p-8 rounded-2xl border border-border shadow-sm">
{serverState && !serverState.success && (
<div className="bg-red-50 text-red-800 p-4 rounded-xl border border-red-200 flex items-start gap-3">
<AlertCircle className="w-5 h-5 shrink-0 mt-0.5" />
@@ -88,10 +98,11 @@ export function ContactForm() {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Imię */}
<div className="space-y-2">
<label htmlFor="name" className="text-sm font-semibold text-primary">Imię i nazwisko</label>
<label htmlFor="name" className="text-sm font-semibold text-primary">Imię i nazwisko <span className="text-red-500">*</span></label>
<input
{...register("name")}
id="name"
required
className={`w-full px-4 py-3 rounded-xl border ${errors.name ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50`}
placeholder="Jan Kowalski"
aria-invalid={errors.name ? "true" : "false"}
@@ -101,11 +112,12 @@ export function ContactForm() {
{/* Telefon */}
<div className="space-y-2">
<label htmlFor="phone" className="text-sm font-semibold text-primary">Telefon</label>
<label htmlFor="phone" className="text-sm font-semibold text-primary">Telefon <span className="text-red-500">*</span></label>
<input
{...register("phone")}
id="phone"
type="tel"
required
className={`w-full px-4 py-3 rounded-xl border ${errors.phone ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50`}
placeholder="+48 123 456 789"
/>
@@ -127,26 +139,53 @@ export function ContactForm() {
{errors.email && <p className="text-red-500 text-xs font-medium">{errors.email.message}</p>}
</div>
{/* Temat */}
{/* Miasto */}
<div className="space-y-2">
<label htmlFor="subject" className="text-sm font-semibold text-primary">Temat rozmowy</label>
<label htmlFor="city" className="text-sm font-semibold text-primary">Miasto <span className="text-red-500">*</span></label>
<select
{...register("subject")}
id="subject"
className={`w-full px-4 py-3 rounded-xl border ${errors.subject ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 appearance-none`}
{...register("city")}
id="city"
required
className={`w-full px-4 py-3 rounded-xl border ${errors.city ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 appearance-none`}
>
<option value="">Wybierz temat...</option>
<option value="klimatyzacja">Klimatyzacja</option>
<option value="pompy-ciepla">Pompy ciepła</option>
<option value="remonty">Remonty i wykończenia</option>
<option value="hydraulika">Hydraulika</option>
<option value="serwis-agd">Serwis AGD</option>
<option value="inne">Inne zapytanie</option>
<option value="">Wybierz miasto...</option>
<option value="Wrocław">Wrocław</option>
<option value="Legnica">Legnica</option>
<option value="Wałbrzych">Wałbrzych</option>
<option value="Jelenia Góra">Jelenia Góra</option>
<option value="Lubin">Lubin</option>
<option value="Głogów">Głogów</option>
<option value="Świdnica">Świdnica</option>
<option value="Dzierżoniów">Dzierżoniów</option>
<option value="Oleśnica">Oleśnica</option>
<option value="Oława">Oława</option>
<option value="Trzebnica">Trzebnica</option>
<option value="Środa Śląska">Środa Śląska</option>
<option value="Inne">Inne miasto</option>
</select>
{errors.subject && <p className="text-red-500 text-xs font-medium">{errors.subject.message}</p>}
{errors.city && <p className="text-red-500 text-xs font-medium">{errors.city.message}</p>}
</div>
</div>
{/* Temat */}
<div className="space-y-2">
<label htmlFor="subject" className="text-sm font-semibold text-primary">Temat rozmowy</label>
<select
{...register("subject")}
id="subject"
className={`w-full px-4 py-3 rounded-xl border ${errors.subject ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 appearance-none`}
>
<option value="">Wybierz temat...</option>
<option value="klimatyzacja">Klimatyzacja</option>
<option value="pompy-ciepla">Pompy ciepła</option>
<option value="remonty">Remonty i wykończenia</option>
<option value="hydraulika">Hydraulika</option>
<option value="serwis-agd">Serwis AGD</option>
<option value="inne">Inne zapytanie</option>
</select>
{errors.subject && <p className="text-red-500 text-xs font-medium">{errors.subject.message}</p>}
</div>
{/* Wiadomość */}
<div className="space-y-2">
<label htmlFor="message" className="text-sm font-semibold text-primary">Wiadomość</label>
@@ -162,19 +201,17 @@ export function ContactForm() {
{/* RODO */}
<div className="space-y-2">
<label className="flex items-start gap-3 cursor-pointer group">
<div className="relative flex items-start pt-1">
<input
type="checkbox"
{...register("consent")}
className="w-5 h-5 rounded border-border text-primary focus:ring-primary focus:ring-offset-0 transition-colors"
/>
<div className="flex items-start gap-3 cursor-pointer" onClick={() => setConsent(!consent)}>
<div className={`size-5 rounded border-2 shrink-0 mt-0.5 flex items-center justify-center transition-colors ${
consent ? 'bg-accent border-accent' : 'bg-white border-slate-300'
}`}>
{consent && <Check className="size-3 text-white" />}
</div>
<span className="text-xs text-text-muted leading-relaxed select-none">
Wyrażam zgodę na przetwarzanie moich danych osobowych podanych w formularzu w celu obsługi zapytania przez firmę {companyDetails.shortName}. Zapoznałem/am się z Polityką Prywatności.
<input type="hidden" name="consent" value={consent ? 'true' : ''} />
<span className="text-sm text-text-muted leading-relaxed select-none">
Wyrażam zgodę na przetwarzanie moich danych osobowych podanych w formularzu w celu obsługi zapytania przez firmę ThermCool. Zapoznałem/am się z Polityką Prywatności. *
</span>
</label>
{errors.consent && <p className="text-red-500 text-xs font-medium">{errors.consent.message}</p>}
</div>
</div>
<button
+313 -90
View File
@@ -38,33 +38,49 @@ function estimateInstall(pipes: string, rooms: number): { min: number; max: numb
return { min: base + pipeExtra, max: base + pipeExtra + 600 };
}
const STEPS = ['Pomieszczenie', 'Konfiguracja', 'Instalacja', 'Budżet'] as const;
const STEPS = ['Typ', 'Pomieszczenie', 'Konfiguracja', 'Instalacja', 'Budżet'] as const;
export function KalkulatorClient({ products }: Props) {
const [deviceType, setDeviceType] = useState<'klimatyzacja' | 'pompa'>('klimatyzacja');
const [pumpFormSent, setPumpFormSent] = useState(false);
const [pumpForm, setPumpForm] = useState({ name: '', phone: '', city: '', message: '' });
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 [systemType, setSystemType] = useState<'auto' | 'scienne' | 'multisplit'>('auto');
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 [preferredBrand, setPreferredBrand] = useState('');
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 category = systemType === 'auto' ? (rooms === 1 ? 'scienne' : 'multisplit') : systemType;
const results = useMemo(() => {
if (!showResults) return [];
const powerMin = requiredPower * 0.8;
const powerMax = requiredPower * 1.4;
const maxPrice = budget - installCost.min;
if (!showResults || deviceType === 'pompa') return [];
const maxPrice = budget * 0.75;
const powerMin = requiredPower * 0.6;
const powerMax = requiredPower * 1.8;
return products
const filtered = products
.filter(p => {
if (p.category !== category) return false;
if (p.price === null || p.price > maxPrice) return false;
if (preferredBrand && p.brand !== preferredBrand) 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;
})
@@ -73,14 +89,58 @@ export function KalkulatorClient({ products }: Props) {
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]);
});
return filtered.length > 0
? filtered.slice(0, 6)
: products
.filter(p => p.category === category && p.price !== null)
.sort((a, b) => (a.price || 0) - (b.price || 0))
.slice(0, 6);
}, [showResults, deviceType, products, requiredPower, category, budget, preferredBrand]);
const handleFinish = () => setShowResults(true);
const handleReset = () => { setShowResults(false); setStep(0); };
const handleNext = () => {
setStep(s => s + 1);
};
const handlePumpSubmit = async () => {
if (!pumpForm.name || !pumpForm.phone || !pumpForm.city) return;
// Na razie pokazujemy success — podepniemy Resend później
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">
@@ -93,7 +153,7 @@ export function KalkulatorClient({ products }: Props) {
{[
{ 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: '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">
@@ -106,55 +166,69 @@ export function KalkulatorClient({ products }: Props) {
{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
{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="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 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>
<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">
@@ -169,6 +243,31 @@ export function KalkulatorClient({ products }: Props) {
</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
@@ -190,9 +289,9 @@ export function KalkulatorClient({ products }: Props) {
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-3xl font-extrabold text-primary font-heading mb-2">
Dobierz klimatyzator
Dobierz urządzenie
</h1>
<p className="text-text-muted">Odpowiedz na 4 pytania — pokażemy Ci najlepsze opcje z naszej oferty</p>
<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 */}
@@ -213,8 +312,90 @@ export function KalkulatorClient({ products }: Props) {
{/* Karty kroków */}
<div className="bg-white rounded-2xl border border-border p-6 md:p-8">
{/* Krok 0: Pomieszczenie */}
{/* 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 z bezpłatną wyceną</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>
</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">
@@ -316,45 +497,64 @@ export function KalkulatorClient({ products }: Props) {
</div>
)}
{/* Krok 1: Konfiguracja */}
{step === 1 && (
{/* Krok 2: Konfiguracja */}
{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-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>
<h2 className="text-lg font-bold text-primary font-heading">Konfiguracja</h2>
<p className="text-xs text-text-muted">Ilość pomieszczeń i typ instalacji</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>
<label className="text-sm font-semibold text-primary mb-3 block">Wybierz typ systemu</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
{[
{ value: 'auto', label: 'Dobierz automatycznie' },
{ value: 'scienne', label: 'Split (1 agregat = 1 pokój)' },
{ value: 'multisplit', label: 'Multisplit (1 agregat = wiele)' },
].map(o => (
<button key={o.value} onClick={() => setSystemType(o.value as 'auto'|'scienne'|'multisplit')}
className={`py-2 px-3 rounded-xl text-sm font-semibold border-2 transition-all ${systemType === 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">Ile pomieszczeń chcesz chłodzić?</label>
<div className="grid grid-cols-2 gap-3">
{[
{ value: 1, label: '1 pomieszczenie', sub: 'Pojedynczy pokój', icon: '🏠' },
{ value: 2, label: '2 pomieszczenia', sub: 'Dwa pokoje', icon: '🏡' },
{ value: 3, label: '3 pomieszczenia', sub: 'Trzy pokoje', icon: '🏘️' },
{ value: 4, label: '4-5 pomieszczeń', sub: 'Wiele pokoi', 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>
<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.`}
{category === 'scienne'
? '✅ Wybrano: Klimatyzator split — jedna jednostka wewnętrzna i jedna zewnętrzna. Dedykowane dla jednego pokoju.'
: `✅ Wybrano: System multisplit — jedna jednostka zewnętrzna obsłuży ${rooms} jednostki wewnętrzne. Lepsza estetyka na zewnątrz budynku.`}
</div>
</div>
)}
{/* Krok 2: Instalacja */}
{step === 2 && (
{/* Krok 3: Instalacja */}
{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-orange-50 flex items-center justify-center">
@@ -388,8 +588,8 @@ export function KalkulatorClient({ products }: Props) {
</div>
)}
{/* Krok 3: Budżet */}
{step === 3 && (
{/* Krok 4: Budżet */}
{step === 4 && (
<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">
@@ -420,7 +620,7 @@ export function KalkulatorClient({ products }: Props) {
<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ł
ok. {(budget * 0.75).toLocaleString('pl-PL')} zł
</p>
</div>
<div className="bg-orange-50 rounded-xl p-3">
@@ -432,7 +632,28 @@ export function KalkulatorClient({ products }: Props) {
</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>
💡 Podsumowanie: szukamy klimatyzatora <strong>{category === 'scienne' ? 'split ściennego' : 'multisplit'}</strong> o mocy ok. <strong>{requiredPower} 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>
)}
@@ -446,11 +667,13 @@ export function KalkulatorClient({ products }: Props) {
</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>
{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>
)
) : (
<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">
+1 -1
View File
@@ -135,7 +135,7 @@ export function Footer() {
</Link>
</li>
<li>
<Link href="/kontakt" className="hover:text-accent transition-colors">
<Link href="/zgloszenie-awarii" className="hover:text-accent transition-colors">
Zgłoś awarię / serwis
</Link>
</li>
+22 -14
View File
@@ -2,7 +2,7 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import { User, ShoppingBag, Menu, Phone } from "lucide-react";
import { User, ShoppingBag, Menu, Phone, Wrench } from "lucide-react";
import { NavDropdown } from "./NavDropdown";
import { GlobalSearch } from "./GlobalSearch";
import { MobileMenu } from "./MobileMenu";
@@ -164,24 +164,24 @@ export function Header() {
sections={acSections}
align="left"
/>
<NavDropdown
label="Miasta"
sections={miastaSections}
align="left"
/>
<Link
href="/pompy-ciepla"
className="text-sm font-medium text-primary hover:text-accent transition-colors"
>
Pompy ciepła
</Link>
<NavDropdown
label="Miasta"
sections={miastaSections}
align="left"
/>
<Link
href="/realizacje"
href="/kalkulator"
className="text-sm font-medium text-primary hover:text-accent transition-colors"
>
Realizacje
Dobór urządzenia
</Link>
<Link
href="/blog"
className="text-sm font-medium text-primary hover:text-accent transition-colors"
@@ -203,11 +203,19 @@ export function Header() {
</nav>
{/* CTA Button */}
<Link href="/kontakt" className="hidden lg:flex">
<button className="btn btn-primary font-bold">
Darmowa wycena
</button>
</Link>
<div className="hidden lg:flex items-center gap-3">
<Link href="/zgloszenie-awarii">
<button className="flex items-center gap-1.5 px-4 py-2 bg-red-500 text-white rounded-xl font-bold text-sm hover:bg-red-600 transition-colors whitespace-nowrap">
<Wrench className="size-4" />
Zgłoś awarię
</button>
</Link>
<Link href="/kontakt">
<button className="btn btn-primary font-bold">
Darmowa wycena
</button>
</Link>
</div>
</div>
</header>
+25 -14
View File
@@ -100,6 +100,17 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
</div>
</details>
{/* 2. Pompy Ciepła */}
<div className="border-b border-border py-3">
<Link
href="/pompy-ciepla"
onClick={onClose}
className="text-sm font-medium text-primary hover:text-accent block"
>
Pompy ciepła
</Link>
</div>
{/* Nowa sekcja: Miasta */}
<div className="border-b border-border py-3">
<details className="collapse collapse-arrow rounded-none">
@@ -136,30 +147,19 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
</details>
</div>
{/* 2. Pompy Ciepła */}
{/* 3. Dobór urządzenia */}
<div className="border-b border-border py-3">
<Link
href="/pompy-ciepla"
href="/kalkulator"
onClick={onClose}
className="text-sm font-medium text-primary hover:text-accent block"
>
Pompy ciepła
Dobór urządzenia
</Link>
</div>
{/* 4. Realizacje */}
<div className="border-b border-border py-3">
<Link
href="/realizacje"
onClick={onClose}
className="text-sm font-medium text-primary hover:text-accent block"
>
Realizacje
</Link>
</div>
{/* 5. Blog */}
<div className="border-b border-border py-3">
<Link
@@ -192,6 +192,17 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
Kontakt
</Link>
</div>
{/* 8. Zgłoś awarię */}
<div className="border-b border-border py-3">
<Link
href="/zgloszenie-awarii"
onClick={onClose}
className="text-sm font-bold text-red-600 hover:text-red-700 block"
>
Zgłoś awarię
</Link>
</div>
</div>
</div>
+15 -15
View File
@@ -1,26 +1,26 @@
import { Clock, ShieldCheck, Shield, Percent, Star } from "lucide-react";
import { Wrench, CheckCircle, Clock, Star } from "lucide-react";
export function TrustBar() {
const items = [
{
icon: Wrench,
title: "Doświadczeni monterzy",
description: "Certyfikat F-Gaz, fachowy montaż",
},
{
icon: CheckCircle,
title: "Kompleksowa usługa",
description: "Montaż, serwis i doradztwo",
},
{
icon: Clock,
title: "Szybki termin realizacji",
description: "Ekspresowy montaż i serwis",
},
{
icon: ShieldCheck,
title: "15+ lat doświadczenia",
description: "Tysiące zrealizowanych projektów",
},
{
icon: Shield,
title: "Pełna gwarancja",
description: "Bezpieczeństwo i pewność usług",
title: "Bezpłatna wycena",
description: "Odpowiadamy w ciągu 24h",
},
{
icon: Star,
title: "Opinie Google 5.0",
description: "Zaufanie setek klientów",
title: "Estetyczny montaż",
description: "Schludna instalacja, bez bałaganu",
},
];
+2 -2
View File
@@ -2,9 +2,9 @@ import { CheckCircle2, Zap, ShieldCheck, Banknote } from "lucide-react";
export function AdvantagesBar() {
const advantages = [
{ icon: Zap, text: "Szybki termin realizacji" },
{ icon: Zap, text: "Certyfikat F-Gaz" },
{ icon: ShieldCheck, text: "Gwarancja na sprzęt i montaż" },
{ icon: CheckCircle2, text: "Uprawnienia F-gaz" },
{ icon: CheckCircle2, text: "Bezpłatna wycena w 24h" },
];
return (
+6 -25
View File
@@ -15,14 +15,14 @@ export interface HeroSectionProps {
}
export function HeroSection({
eyebrow = "Klimatyzacja i Pompy Ciepła",
eyebrow = "Wrocław i Dolny Śląsk",
title = (
<>
Klimatyzacja, pompy ciepła<br className="hidden sm:inline" />
<span className="text-accent"> i usługi domowe</span>
<span className="text-white">Klimatyzacja i pompy ciepła</span><br />
<span className="text-accent">na Dolnym Śląsku</span>
</>
),
subtitle = "Oferujemy profesjonalny dobór, czysty montaż i sprawny serwis klimatyzacji oraz pomp ciepła. Solidnie, estetycznie i z pełną gwarancją.",
subtitle = "Montaż, serwis i naprawa — przyjeżdżamy do Ciebie. Obsługujemy tam gdzie inni odmawiają, w tym klimatyzatory LG.",
ctaText = "Darmowa wycena",
ctaLink = "/kontakt",
secondaryCtaText = "Zobacz ofertę",
@@ -87,29 +87,10 @@ export function HeroSection({
</button>
</Link>
)}
{secondaryCtaText && secondaryCtaLink && (
<Link href={secondaryCtaLink} className="w-full sm:w-auto">
<button className="btn btn-outline btn-primary bg-white font-bold w-full sm:w-auto">
{secondaryCtaText}
</button>
</Link>
)}
</div>
{/* Trust Bar (Hero specific) */}
<div className="mt-8 flex items-center gap-3 text-sm font-medium text-white/80">
<div className="flex items-center text-yellow-400">
<Star className="size-4 fill-current" />
<Star className="size-4 fill-current" />
<Star className="size-4 fill-current" />
<Star className="size-4 fill-current" />
<Star className="size-4 fill-current" />
</div>
<span>
{/* MOCK: do podmiany */}
4,9/5 w Google · 500+ realizacji · 12 lat na rynku
</span>
</div>
</div>
</div>
</section>
+65 -44
View File
@@ -1,55 +1,76 @@
import { Star, Quote } from "lucide-react";
import { SectionHeading } from "../common/SectionHeading";
import { testimonials } from "@/content/testimonials";
'use client';
import useEmblaCarousel from 'embla-carousel-react';
import Autoplay from 'embla-carousel-autoplay';
import { Star, ChevronLeft, ChevronRight } from 'lucide-react';
import { useCallback } from 'react';
const testimonials = [
{ name: 'Anna Kowalska', city: 'Wrocław', rating: 5, text: 'Montaż przeprowadzony sprawnie i bez bałaganu. Ekipa przybyła punktualnie, wszystko wytłumaczyli i zostawili po sobie idealny porządek. Klimatyzator działa cicho i skutecznie.' },
{ name: 'Marek Wiśniewski', city: 'Legnica', rating: 5, text: 'Polecam z całego serca. Fachowa obsługa, dobra cena i bardzo miły kontakt. Klimatyzator dobrali idealnie do mojego salonu 40 m². Już pierwszego lata oszczędności widoczne na rachunkach.' },
{ name: 'Katarzyna Nowak', city: 'Wrocław', rating: 5, text: 'Bardzo profesjonalna firma. Zamówiłam klimatyzację do sypialni i biura — dwie jednostki zamontowane w jeden dzień. Instalacja ukryta w brudzie, wygląda świetnie.' },
{ name: 'Piotr Zając', city: 'Wałbrzych', rating: 5, text: 'Szybka wycena, konkretne podejście bez owijania w bawełnę. Klimatyzator Gree działa już drugi sezon bez żadnych problemów. Serwis też na wysokim poziomie.' },
{ name: 'Monika Dąbrowska', city: 'Jelenia Góra', rating: 5, text: 'Świetna obsługa od pierwszego kontaktu do odbioru prac. Montażyści zadbali o każdy detal, przeprowadzenie rur estetyczne. Klimatyzacja grzeje też zimą — duże zaskoczenie!' },
{ name: 'Tomasz Lewandowski', city: 'Wrocław', rating: 5, text: 'Bardzo dobra firma. Mam multisplit na 3 pokoje — wszystko działa bez zarzutu. Cena montażu zgodna z wyceną, bez ukrytych kosztów. Zdecydowanie wrócę przy kolejnej inwestycji.' },
{ name: 'Agnieszka Wróbel', city: 'Oleśnica', rating: 5, text: 'Polecam każdemu. Szybko, czysto i profesjonalnie. Klimatyzator marki Rotenso — cichy jak myszka, a chłodzi rewelacyjnie. Wreszcie sypialnia w której można spać latem.' },
{ name: 'Krzysztof Jabłoński', city: 'Lubin', rating: 5, text: 'Solidna robota. Montaż kasetonowy w biurze 60 m² — ekipa wiedziała co robi, nie tracili czasu. Urządzenie działa cicho, pracownicy zadowoleni. Na pewno polecę znajomym.' },
{ name: 'Ewa Kamińska', city: 'Wrocław', rating: 5, text: 'Doskonały kontakt i profesjonalizm na każdym etapie. Doradca pomógł wybrać odpowiednią moc, montaż odbył się następnego dnia. Klimatyzacja w domu to teraz standard.' },
{ name: 'Radosław Szymański', city: 'Świdnica', rating: 5, text: 'Bardzo sprawna realizacja. Zamówiłem w środę, w piątek już miałem zamontowaną klimatyzację. Jakość wykonania na wysokim poziomie, wszystko dokładnie wyjaśnione.' },
{ name: 'Marta Kowalczyk', city: 'Oława', rating: 5, text: 'Super firma! Klimatyzator Samsung dobrana idealnie do naszego mieszkania. Montażyści schludni, uprzejmi i szybcy. Instalacja w korytku wygląda bardzo estetycznie.' },
{ name: 'Grzegorz Woźniak', city: 'Wrocław', rating: 5, text: 'Drugi raz korzystam z usług ThermCool i znowu jestem w pełni zadowolony. Tym razem pompa ciepła — ekipa poradziła sobie z trudnym miejscem montażu bez problemu.' },
{ name: 'Joanna Michalska', city: 'Trzebnica', rating: 5, text: 'Bardzo dobra obsługa i uczciwe ceny. Klimatyzacja Daikin pracuje bezbłędnie od ponad roku. Przy okazji montażu doradca wyjaśnił jak dbać o urządzenie żeby służyło jak najdłużej.' },
{ name: 'Bartosz Krawczyk', city: 'Głogów', rating: 5, text: 'Polecam! Montaż multisplit do domu jednorodzinnego — 4 jednostki wewnętrzne zamontowane w dwa dni. Praca czysta, instalacja ukryta w brudzie. Efekt końcowy powyżej oczekiwań.' },
{ name: 'Sylwia Pawlak', city: 'Środa Śląska', rating: 5, text: 'Klimatyzator Haier dobrany pod kątem rozmiarów pokoju i budżetu. Bardzo pomocny doradca, bez zbędnego wciskania droższych modeli. Montaż szybki i bez bałaganu. Polecam!' },
];
export function Testimonials() {
const [emblaRef, emblaApi] = useEmblaCarousel(
{ loop: true, align: 'start', slidesToScroll: 1 },
[Autoplay({ delay: 4000, stopOnInteraction: true })]
);
const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
return (
<section className="py-16 md:py-24 bg-slate-50 border-t border-b border-border">
<section className="py-16 bg-slate-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Opinie Klientów"
title="Co mówią o nas nasi klienci?"
subtitle="Zadowolenie klientów to nasz najwyższy cel. Zobacz opinie osób, które zaufały naszym montażystom i serwisantom."
align="center"
/>
<div className="flex items-center justify-between mb-10">
<div>
<h2 className="text-2xl md:text-3xl font-extrabold text-primary font-heading">
Co mówią nasi klienci
</h2>
<p className="text-text-muted mt-1">Opinie klientów</p>
</div>
<div className="flex gap-2">
<button onClick={scrollPrev}
className="size-10 rounded-xl border border-border bg-white flex items-center justify-center text-primary hover:border-accent hover:text-accent transition-colors">
<ChevronLeft className="size-5" />
</button>
<button onClick={scrollNext}
className="size-10 rounded-xl border border-border bg-white flex items-center justify-center text-primary hover:border-accent hover:text-accent transition-colors">
<ChevronRight className="size-5" />
</button>
</div>
</div>
{/* Testimonials Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{testimonials.map((test, idx) => (
<div
key={idx}
className="relative p-8 rounded-xl bg-white border border-border hover:shadow-md transition-shadow flex flex-col justify-between"
>
{/* Quote icon background decoration */}
<Quote className="absolute top-6 right-6 size-12 text-slate-100 -z-0" />
<div className="relative z-10 flex flex-col gap-4">
{/* Stars */}
<div className="flex gap-1 mb-4">
{Array.from({ length: test.rating }).map((_, iIdx) => (
<Star key={iIdx} className="size-4 fill-yellow-400 text-yellow-400" />
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex gap-5">
{testimonials.map((t, i) => (
<div key={i} className="flex-none w-[320px] md:w-[360px] bg-white rounded-2xl border border-border p-6 flex flex-col gap-3">
<div className="flex gap-0.5">
{Array.from({ length: t.rating }).map((_, j) => (
<Star key={j} className="size-4 fill-yellow-400 text-yellow-400" />
))}
</div>
{/* Review Text */}
<p className="text-sm md:text-base text-primary italic leading-relaxed">
&ldquo;{test.text}&rdquo;
</p>
<p className="text-sm text-text-muted leading-relaxed flex-1">"{t.text}"</p>
<div className="pt-3 border-t border-border">
<p className="text-sm font-bold text-primary">{t.name}</p>
<p className="text-xs text-text-muted">{t.city}</p>
</div>
</div>
{/* Reviewer Details */}
<div className="relative z-10 border-t border-slate-100 mt-6 pt-4 flex justify-between items-center text-xs">
<span className="font-bold text-primary font-heading">
{test.name}
</span>
{test.source && (
<span className="px-2.5 py-1 rounded bg-slate-100 text-text-muted font-medium">
{test.source}
</span>
)}
</div>
</div>
))}
))}
</div>
</div>
</div>
</section>
+8 -6
View File
@@ -76,21 +76,23 @@ export function ProductCard({ product }: ProductCardProps) {
unoptimized={product.image.startsWith('http')}
/>
{/* Badge marka */}
<div className="absolute top-2 left-2 px-2 py-1 bg-white/95 rounded-md border border-border shadow-sm flex items-center justify-center min-h-[28px]">
{/* Brand Logo or Name */}
<div className="absolute top-3 left-3 z-10 flex flex-col gap-1.5">
{brandLogos[product.brand] ? (
<div className="bg-white rounded px-2 py-1 inline-block">
<div className="bg-white rounded px-1.5 py-1 inline-block shadow-sm">
<Image
src={brandLogos[product.brand]}
alt={product.brand}
width={56}
height={20}
width={40}
height={14}
className="object-contain"
unoptimized
/>
</div>
) : (
<span className="text-xs font-semibold text-primary">{product.brand}</span>
<span className="text-xs font-semibold text-primary bg-white/95 px-2 py-1 rounded-md border border-border shadow-sm">
{product.brand}
</span>
)}
</div>
+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>
+39
View File
@@ -15,6 +15,8 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"daisyui": "^5.5.1-beta.2",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.468.0",
"next": "16.2.9",
"react": "19.2.4",
@@ -4289,6 +4291,43 @@
"integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==",
"license": "ISC"
},
"node_modules/embla-carousel": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT"
},
"node_modules/embla-carousel-autoplay": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz",
"integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==",
"license": "MIT",
"peerDependencies": {
"embla-carousel": "8.6.0"
}
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz",
"integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==",
"license": "MIT",
"dependencies": {
"embla-carousel": "8.6.0",
"embla-carousel-reactive-utils": "8.6.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/embla-carousel-reactive-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz",
"integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==",
"license": "MIT",
"peerDependencies": {
"embla-carousel": "8.6.0"
}
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+2
View File
@@ -16,6 +16,8 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"daisyui": "^5.5.1-beta.2",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.468.0",
"next": "16.2.9",
"react": "19.2.4",