feat: Zaktualizuj sekcję hero, nawigację, kalkulator i formularz zgłoszenia awarii
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,8 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
'/regulamin',
|
||||
'/polityka-prywatnosci',
|
||||
'/reklamacje',
|
||||
'/serwis-lg',
|
||||
'/zgloszenie-awarii',
|
||||
'/dostawa',
|
||||
'/gwarancja',
|
||||
].map((route) => ({
|
||||
|
||||
@@ -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
@@ -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} />
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
“{test.text}”
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Generated
+39
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user