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

This commit is contained in:
MichalC
2026-06-28 15:03:36 +02:00
parent 002a325dfb
commit 59808f1243
22 changed files with 1465 additions and 345 deletions
+9 -3
View File
@@ -10,9 +10,10 @@ const resend = new Resend(process.env.RESEND_API_KEY);
const contactFormSchema = z.object({ const contactFormSchema = z.object({
name: z.string().min(2, "Imię musi mieć min. 2 znaki"), name: z.string().min(2, "Imię musi mieć min. 2 znaki"),
phone: z.string().min(9, "Wprowadź poprawny numer telefonu"), phone: z.string().min(9, "Wprowadź poprawny numer telefonu"),
email: z.string().email("Niepoprawny adres e-mail"), email: z.union([z.literal(''), z.string().email("Niepoprawny adres e-mail")]).optional(),
subject: z.string().min(1, "Wybierz temat"), city: z.string().min(1, "Wybierz miasto"),
message: z.string().min(10, "Wiadomość musi mieć min. 10 znaków"), subject: z.string().optional(),
message: z.string().optional(),
consent: z.boolean().refine((val) => val === true, "Zgoda RODO jest wymagana"), 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"), name: formData.get("name"),
phone: formData.get("phone"), phone: formData.get("phone"),
email: formData.get("email"), email: formData.get("email"),
city: formData.get("city"),
subject: formData.get("subject"), subject: formData.get("subject"),
message: formData.get("message"), message: formData.get("message"),
consent: formData.get("consent") === "true" || formData.get("consent") === "on", 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;"><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> <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>
<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> <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;"><strong>Temat:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.subject}</td> <td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.subject}</td>
+37
View File
@@ -7,6 +7,8 @@ import { Testimonials } from "@/components/sections/Testimonials";
import { BlogPreview } from "@/components/sections/BlogPreview"; import { BlogPreview } from "@/components/sections/BlogPreview";
import { CtaBanner } from "@/components/sections/CtaBanner"; import { CtaBanner } from "@/components/sections/CtaBanner";
import { companyDetails } from "@/content/company"; import { companyDetails } from "@/content/company";
import { Wrench, Phone, ArrowRight } from "lucide-react";
import Link from "next/link";
export default function Home() { export default function Home() {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl'; const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
@@ -139,6 +141,41 @@ export default function Home() {
<WhyUs /> <WhyUs />
{/* ProjectsSection will go here in Group B */} {/* ProjectsSection will go here in Group B */}
<Testimonials /> <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 /> <BlogPreview />
<CtaBanner bgVariant="accent-light" /> <CtaBanner bgVariant="accent-light" />
</> </>
+200
View File
@@ -0,0 +1,200 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { Check, Phone, Wrench, AlertTriangle, ArrowRight } from 'lucide-react';
import { companyDetails } from '@/content/company';
export const metadata: Metadata = {
title: 'Serwis klimatyzacji LG Wrocław — naprawa i przegląd | ThermCool',
description: 'Serwisujemy klimatyzatory LG na Dolnym Śląsku. Naprawa, przegląd, odgrzybianie — tam gdzie inne firmy odmawiają. Zadzwoń i umów wizytę.',
alternates: { canonical: '/serwis-lg' },
openGraph: {
title: 'Serwis klimatyzacji LG Wrocław | ThermCool',
description: 'Specjalizujemy się w serwisie klimatyzatorów LG. Naprawa, diagnostyka, przegląd na Dolnym Śląsku.',
},
};
const faqItems = [
{
q: 'Dlaczego inne firmy odmawiają serwisu klimatyzacji LG?',
a: 'Klimatyzatory LG wymagają specjalistycznej wiedzy i dedykowanych narzędzi diagnostycznych. Wiele firm unika tych urządzeń ze względu na złożoność układów elektronicznych i trudniejszy dostęp do części zamiennych. My posiadamy odpowiednie doświadczenie i narzędzia.',
},
{
q: 'Ile kosztuje przegląd klimatyzatora LG?',
a: 'Koszt przeglądu klimatyzatora LG zaczyna się od 250 zł i obejmuje czyszczenie filtrów, sprawdzenie ciśnienia czynnika chłodniczego, kontrolę układu elektrycznego i test działania. Dokładna wycena zależy od modelu i stanu urządzenia.',
},
{
q: 'Jak długo trwa naprawa klimatyzatora LG?',
a: 'Standardowy przegląd i czyszczenie to 1-2 godziny. Naprawa usterek zależy od rodzaju problemu — prostsze naprawy wykonujemy podczas jednej wizyty, przy bardziej skomplikowanych usterkach czas realizacji ustalamy indywidualnie po diagnostyce.',
},
{
q: 'Czy przyjeżdżacie poza Wrocław?',
a: 'Tak, obsługujemy cały Dolny Śląsk — Legnicę, Wałbrzych, Jelenią Górę, Lubin, Świdnicę i okoliczne miejscowości. Koszt dojazdu ustalamy indywidualnie.',
},
{
q: 'Czy naprawiacie wszystkie modele LG?',
a: 'Serwisujemy większość klimatyzatorów LG dostępnych na rynku, w tym modele z serii Artcool, Standard Plus, Dualcool i MultiSplit. W przypadku bardzo starych lub rzadkich modeli skontaktuj się z nami przed umówieniem wizyty.',
},
];
export default function SerwisLGPage() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Service',
name: 'Serwis klimatyzacji LG',
provider: {
'@type': 'HVACBusiness',
name: companyDetails.name,
telephone: companyDetails.phone,
address: {
'@type': 'PostalAddress',
addressLocality: companyDetails.address.city,
addressRegion: companyDetails.address.region,
addressCountry: 'PL',
},
},
areaServed: { '@type': 'AdministrativeArea', name: 'Dolny Śląsk' },
description: 'Serwis, naprawa i przegląd klimatyzatorów LG na Dolnym Śląsku.',
mainEntityOfPage: {
'@type': 'FAQPage',
mainEntity: faqItems.map(f => ({
'@type': 'Question',
name: f.q,
acceptedAnswer: { '@type': 'Answer', text: f.a },
})),
},
};
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<main>
{/* Hero */}
<section className="bg-primary text-white py-16 md:py-24">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-white/10 rounded-full text-sm font-medium mb-6">
<Wrench className="size-4" />
Specjalistyczny serwis LG
</div>
<h1 className="text-3xl md:text-5xl font-extrabold font-heading leading-tight mb-6">
Serwis klimatyzacji LG<br />
<span className="text-accent">tam gdzie inni mówią nie</span>
</h1>
<p className="text-lg text-white/80 mb-8 max-w-2xl">
Klimatyzatory LG mają opinię trudnych w serwisie — większość firm odmawia ich naprawy.
My mamy odpowiednie doświadczenie i narzędzia, żeby skutecznie diagnozować
i naprawiać te urządzenia na terenie Dolnego Śląska.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<a href={`tel:${companyDetails.phoneRaw}`}>
<button className="flex items-center gap-2 px-6 py-3 bg-accent text-white rounded-xl font-bold hover:bg-accent/90 transition-colors">
<Phone className="size-5" /> Zadzwoń: {companyDetails.phone}
</button>
</a>
<Link href="/kontakt">
<button className="flex items-center gap-2 px-6 py-3 bg-white/10 text-white rounded-xl font-bold hover:bg-white/20 transition-colors border border-white/20">
Napisz do nas <ArrowRight className="size-5" />
</button>
</Link>
</div>
</div>
</section>
{/* Dlaczego my */}
<section className="py-16 bg-white">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-start gap-4 p-5 bg-orange-50 border border-orange-200 rounded-2xl mb-10">
<AlertTriangle className="size-6 text-orange-500 shrink-0 mt-0.5" />
<div>
<p className="font-bold text-primary mb-1">Dlaczego inne firmy odmawiają serwisu LG?</p>
<p className="text-sm text-text-muted">
Klimatyzatory LG wymagają specjalistycznej wiedzy i dedykowanych narzędzi diagnostycznych.
Wiele firm serwisowych unika tych urządzeń ze względu na złożoność układów elektronicznych
i trudniejszy dostęp do oryginalnych części zamiennych. My podjęliśmy to wyzwanie
i dziś skutecznie naprawiamy to, od czego inni odchodzą.
</p>
</div>
</div>
<h2 className="text-2xl font-extrabold text-primary font-heading mb-8">Co robimy dla Twojego LG?</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[
{ title: 'Przegląd i czyszczenie', desc: 'Czyszczenie filtrów, lameli, tacy ociekowej. Sprawdzenie szczelności i ciśnienia czynnika R32/R410A.' },
{ title: 'Diagnostyka usterek', desc: 'Odczyt kodów błędów, pomiary elektryczne, identyfikacja uszkodzonych podzespołów.' },
{ title: 'Naprawa układu chłodniczego', desc: 'Uzupełnianie czynnika chłodniczego, naprawa nieszczelności, wymiana sprężarki.' },
{ title: 'Naprawa elektroniki', desc: 'Wymiana płyt głównych, modułów WiFi, czujników temperatury i sterowników.' },
{ title: 'Odgrzybianie i dezynfekcja', desc: 'Profesjonalne odgrzybianie klimatyzatora — eliminacja grzybów i bakterii z układu nawiewu.' },
{ title: 'Serwis gwarancyjny i pogwarancyjny', desc: 'Obsługujemy urządzenia zarówno w trakcie jak i po wygaśnięciu gwarancji producenta.' },
].map((item, i) => (
<div key={i} className="flex gap-3 p-4 bg-slate-50 rounded-xl">
<Check className="size-5 text-accent shrink-0 mt-0.5" />
<div>
<p className="font-bold text-primary text-sm">{item.title}</p>
<p className="text-xs text-text-muted mt-0.5">{item.desc}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* Obsługiwane serie LG */}
<section className="py-16 bg-slate-50">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-extrabold text-primary font-heading mb-6">
Obsługiwane serie klimatyzatorów LG
</h2>
<div className="flex flex-wrap gap-2">
{['LG Artcool', 'LG Standard Plus', 'LG Dualcool', 'LG MultiSplit', 'LG LGRED°', 'LG Inverter V', 'LG UV Nano', 'LG Deluxe'].map(s => (
<span key={s} className="px-4 py-2 bg-white border border-border rounded-xl text-sm font-semibold text-primary">
{s}
</span>
))}
</div>
<p className="text-xs text-text-muted mt-4">
Nie widzisz swojego modelu? Zadzwoń — prawdopodobnie też go obsługujemy.
</p>
</div>
</section>
{/* FAQ */}
<section className="py-16 bg-white">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-extrabold text-primary font-heading mb-8">
Często zadawane pytania
</h2>
<div className="flex flex-col gap-4">
{faqItems.map((item, i) => (
<details key={i} className="group bg-slate-50 rounded-xl border border-border overflow-hidden">
<summary className="flex items-center justify-between p-5 cursor-pointer font-semibold text-primary text-sm list-none">
{item.q}
<ArrowRight className="size-4 text-text-muted group-open:rotate-90 transition-transform shrink-0 ml-3" />
</summary>
<div className="px-5 pb-5 text-sm text-text-muted leading-relaxed border-t border-border pt-4">
{item.a}
</div>
</details>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-16 bg-primary text-white">
<div className="max-w-2xl mx-auto px-4 text-center">
<h2 className="text-2xl font-extrabold font-heading mb-4">
Masz problem z klimatyzatorem LG?
</h2>
<p className="text-white/80 mb-8">
Zadzwoń lub napisz — odpowiadamy szybko i przyjeżdżamy na Dolny Śląsk.
</p>
<a href={`tel:${companyDetails.phoneRaw}`}>
<button className="flex items-center gap-2 px-8 py-4 bg-accent text-white rounded-xl font-bold text-lg hover:bg-accent/90 transition-colors mx-auto">
<Phone className="size-5" /> {companyDetails.phone}
</button>
</a>
</div>
</section>
</main>
</>
);
}
+2
View File
@@ -21,6 +21,8 @@ export default function sitemap(): MetadataRoute.Sitemap {
'/regulamin', '/regulamin',
'/polityka-prywatnosci', '/polityka-prywatnosci',
'/reklamacje', '/reklamacje',
'/serwis-lg',
'/zgloszenie-awarii',
'/dostawa', '/dostawa',
'/gwarancja', '/gwarancja',
].map((route) => ({ ].map((route) => ({
+14 -1
View File
@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation';
import { getProductBySlug, getProducts } from '@/content/products'; import { getProductBySlug, getProducts } from '@/content/products';
import { companyDetails } from '@/content/company'; import { companyDetails } from '@/content/company';
import { ProductPageClient } from '@/components/shop/ProductPageClient'; import { ProductPageClient } from '@/components/shop/ProductPageClient';
import { getProductVariants } from '@/content/product-variants';
interface Props { interface Props {
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
@@ -28,6 +29,13 @@ export default async function ProductPage({ params }: Props) {
const product = getProductBySlug(slug); const product = getProductBySlug(slug);
if (!product) notFound(); 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 baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
const productJsonLd = product.price ? { const productJsonLd = product.price ? {
@@ -54,7 +62,12 @@ export default async function ProductPage({ params }: Props) {
{productJsonLd && ( {productJsonLd && (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }} /> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }} />
)} )}
<ProductPageClient product={product} companyPhone={companyDetails.phone} companyPhoneRaw={companyDetails.phoneRaw} /> <ProductPageClient
product={product}
seriesProducts={seriesProducts}
companyPhone={companyDetails.phone}
companyPhoneRaw={companyDetails.phoneRaw}
/>
</> </>
); );
} }
+2 -2
View File
@@ -23,8 +23,8 @@ export default function ShopPage() {
subtitle={`${splitCount} klimatyzatorów split i ${multiCount} jednostek multisplit. Gree, Rotenso, Haier, Daikin, LG, Samsung i inne marki. Z montażem lub bez.`} 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ż" ctaText="Zapytaj o montaż"
ctaLink="/kontakt" ctaLink="/kontakt"
secondaryCtaText={undefined} secondaryCtaText=""
secondaryCtaLink={undefined} secondaryCtaLink=""
backgroundImage="https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=1600&q=80" backgroundImage="https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=1600&q=80"
/> />
<ProductsGrid products={products} brands={brands} /> <ProductsGrid products={products} brands={brands} />
+59
View File
@@ -0,0 +1,59 @@
'use server';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function submitAwariaForm(data: {
isUrgent: boolean;
brand: string;
model: string;
errorCode: string;
problemType: string;
description: string;
warranty: string;
lastService: string;
name: string;
phone: string;
city: string;
preferredDate: string;
preferredTime: string;
}) {
try {
await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL || '[email protected]',
to: process.env.RESEND_TO_EMAIL || '[email protected]',
subject: `${data.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe: ${data.brand} — ${data.city}`,
html: `
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;">
<div style="background:${data.isUrgent ? '#ef4444' : '#0F2A47'};padding:20px;text-align:center;">
<h1 style="color:#fff;margin:0;font-size:20px;">
${data.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe ThermCool
</h1>
</div>
<div style="padding:24px;">
<h2 style="font-size:16px;margin-top:0;">Dane klienta</h2>
<p><strong>Imię:</strong> ${data.name}</p>
<p><strong>Telefon:</strong> <a href="tel:${data.phone}">${data.phone}</a></p>
<p><strong>Miasto:</strong> ${data.city}</p>
${data.preferredDate ? `<p><strong>Preferowany termin:</strong> ${data.preferredDate} ${data.preferredTime || ''}</p>` : ''}
<h2 style="font-size:16px;">Urządzenie</h2>
<p><strong>Marka:</strong> ${data.brand}</p>
${data.model ? `<p><strong>Model:</strong> ${data.model}</p>` : ''}
${data.warranty ? `<p><strong>Gwarancja:</strong> ${data.warranty}</p>` : ''}
${data.lastService ? `<p><strong>Ostatni serwis:</strong> ${data.lastService}</p>` : ''}
<h2 style="font-size:16px;">Problem</h2>
<p><strong>Typ problemu:</strong> ${data.problemType}</p>
${data.errorCode ? `<p><strong>Kod błędu:</strong> ${data.errorCode}</p>` : ''}
${data.description ? `<p><strong>Opis:</strong> ${data.description}</p>` : ''}
</div>
</div>
`,
});
return { success: true };
} catch {
return { success: false };
}
}
+12
View File
@@ -0,0 +1,12 @@
import type { Metadata } from 'next';
import { AwariaClient } from '@/components/awaria/AwariaClient';
export const metadata: Metadata = {
title: 'Zgłoś awarię klimatyzacji | Serwis ThermCool Wrocław',
description: 'Zgłoś awarię klimatyzatora online. Szybka diagnostyka, kod błędu, preferowany termin wizyty. Obsługujemy Wrocław i cały Dolny Śląsk.',
alternates: { canonical: '/zgloszenie-awarii' },
};
export default function ZgloszenieAwariiPage() {
return <AwariaClient />;
}
+439
View File
@@ -0,0 +1,439 @@
'use client';
import { useState } from 'react';
import { AlertTriangle, Wrench, Phone, Check, Clock, Zap } from 'lucide-react';
import Link from 'next/link';
import { companyDetails } from '@/content/company';
import { submitAwariaForm } from '@/app/zgloszenie-awarii/actions';
// Kody błędów popularnych marek
const ERROR_CODES: Record<string, Record<string, string>> = {
'Gree': {
'E1': 'Błąd czujnika temperatury powietrza wewnętrznego',
'E2': 'Błąd czujnika temperatury wymiennika wewnętrznego',
'E3': 'Błąd czujnika temperatury zewnętrznej',
'E4': 'Wysokie ciśnienie — możliwy problem ze sprężarką lub skraplaczem',
'E5': 'Niskie ciśnienie — możliwy brak czynnika chłodniczego',
'E6': 'Błąd komunikacji między jednostkami',
'F1': 'Awaria czujnika temperatury — wymagana wymiana czujnika',
'H6': 'Blokada silnika wentylatora wewnętrznego',
},
'LG': {
'CH01': 'Błąd czujnika temperatury powietrza wewnętrznego',
'CH02': 'Błąd czujnika wymiennika wewnętrznego',
'CH03': 'Błąd czujnika wymiennika zewnętrznego',
'CH05': 'Błąd komunikacji — sprawdź okablowanie',
'CH07': 'Awaria wentylatora zewnętrznego',
'CH10': 'Błąd czujnika prądu sprężarki',
'CH38': 'Błąd modułu WiFi',
'C1': 'Problem z drenażem — sprawdź odpływ skroplin',
},
'Daikin': {
'A1': 'Awaria PCB — płyta główna jednostki wewnętrznej',
'A5': 'Przegrzanie — wymagane czyszczenie lub sprawdzenie czynnika',
'C4': 'Błąd czujnika wymiennika wewnętrznego',
'E1': 'Awaria PCB jednostki zewnętrznej',
'E3': 'Wyłącznik wysokiego ciśnienia',
'E4': 'Wyłącznik niskiego ciśnienia',
'F3': 'Niskie ciśnienie oleju sprężarki',
'U4': 'Błąd komunikacji wewnętrzna-zewnętrzna',
},
'Samsung': {
'E1': 'Błąd czujnika temperatury powietrza',
'E2': 'Błąd czujnika wymiennika ciepła',
'E3': 'Błąd silnika wentylatora',
'E4': 'Wyłącznik wysokiego ciśnienia',
'E6': 'Błąd komunikacji',
'F1': 'Awaria czujnika — wymagana diagnostyka',
'C4': 'Błąd czujnika temperatury zewnętrznej',
},
'Haier': {
'E1': 'Błąd czujnika temperatury wewnętrznej',
'E2': 'Błąd czujnika wymiennika wewnętrznego',
'E3': 'Błąd czujnika temperatury zewnętrznej',
'E4': 'Wyłącznik wysokiego ciśnienia',
'E5': 'Wyłącznik niskiego ciśnienia — sprawdź poziom czynnika',
'E6': 'Błąd komunikacji',
'F1': 'Awaria czujnika — wymiana',
},
'Rotenso': {
'E1': 'Błąd czujnika temperatury wewnętrznej',
'E2': 'Błąd wymiennika wewnętrznego',
'E3': 'Błąd czujnika zewnętrznego',
'E5': 'Niskie ciśnienie — sprawdź czynnik chłodniczy',
'E6': 'Błąd komunikacji między jednostkami',
'P1': 'Ochrona przed zamrożeniem',
'P4': 'Wyłącznik wysokiego ciśnienia',
},
'Mitsubishi Electric': {
'E1': 'Błąd czujnika wewnętrznego',
'E2': 'Błąd czujnika wymiennika',
'E4': 'Błąd czujnika zewnętrznego',
'E6': 'Problem z wentylatorem zewnętrznym',
'P1': 'Błąd czujnika powietrza zewnętrznego',
'P4': 'Wyłącznik wysokiego ciśnienia',
'P8': 'Przegrzanie sprężarki',
'U1': 'Błąd napięcia zasilania',
},
'Toshiba': {
'E01': 'Błąd czujnika temperatury wewnętrznej',
'E02': 'Błąd czujnika wymiennika wewnętrznego',
'E03': 'Błąd czujnika zewnętrznego',
'E04': 'Wyłącznik wysokiego ciśnienia',
'E07': 'Błąd komunikacji',
'E08': 'Awaria wentylatora zewnętrznego',
},
};
const BRANDS = ['Gree', 'Rotenso', 'Haier', 'LG', 'Samsung', 'Daikin', 'Mitsubishi Electric', 'Toshiba', 'Aux', 'Fuji Electric', 'Inna marka'];
const PROBLEM_TYPES = [
'Nie chłodzi / nie grzeje',
'Wyświetla kod błędu',
'Głośna praca / wibracje',
'Wyciek wody',
'Brzydki zapach',
'Nie włącza się',
'Pilot nie działa',
'Zamarza wymiennik',
'Inne',
];
export function AwariaClient() {
const [step, setStep] = useState<'form' | 'success'>('form');
const [isUrgent, setIsUrgent] = useState(false);
const [brand, setBrand] = useState('');
const [model, setModel] = useState('');
const [errorCode, setErrorCode] = useState('');
const [problemType, setProblemType] = useState('');
const [description, setDescription] = useState('');
const [lastService, setLastService] = useState('');
const [warranty, setWarranty] = useState('');
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [city, setCity] = useState('');
const [preferredDate, setPreferredDate] = useState('');
const [preferredTime, setPreferredTime] = useState('');
const [consent, setConsent] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// Podpowiedź kodu błędu
const errorHint = brand && errorCode && ERROR_CODES[brand]?.[errorCode.toUpperCase()];
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!consent) {
alert('Proszę zaznaczyć zgodę na przetwarzanie danych.');
return;
}
setIsSubmitting(true);
await submitAwariaForm({
isUrgent,
brand,
model,
errorCode,
problemType,
description,
warranty,
lastService,
name,
phone,
city,
preferredDate,
preferredTime,
});
setIsSubmitting(false);
setStep('success');
};
if (step === 'success') {
return (
<main className="bg-slate-50 min-h-screen flex items-center justify-center py-16">
<div className="max-w-md mx-auto px-4 text-center">
<div className="size-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
<Check className="size-10 text-green-600" />
</div>
<h1 className="text-2xl font-extrabold text-primary font-heading mb-3">
Zgłoszenie przyjęte!
</h1>
<p className="text-text-muted mb-2">
{isUrgent
? 'Twoje zgłoszenie oznaczono jako PILNE — skontaktujemy się z Tobą w ciągu 2 godzin.'
: 'Skontaktujemy się z Tobą w ciągu 24 godzin, aby ustalić termin wizyty serwisanta.'}
</p>
<p className="text-sm text-text-muted mb-8">
Jeśli sprawa jest pilna — zadzwoń bezpośrednio:
</p>
<a href={`tel:${companyDetails.phoneRaw}`}>
<button className="flex items-center gap-2 px-6 py-3 bg-accent text-white rounded-xl font-bold mx-auto hover:bg-accent/90 transition-colors mb-4">
<Phone className="size-4" /> {companyDetails.phone}
</button>
</a>
<Link href="/">
<button className="text-sm text-text-muted hover:text-accent transition-colors">
Wróć na stronę główną
</button>
</Link>
</div>
</main>
);
}
return (
<main className="bg-slate-50 min-h-screen py-12">
<div className="max-w-2xl mx-auto px-4 sm:px-6">
{/* Header */}
<div className="text-center mb-8">
<div className="size-14 bg-red-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Wrench className="size-7 text-red-600" />
</div>
<h1 className="text-3xl font-extrabold text-primary font-heading mb-2">
Zgłoś awarię / serwis
</h1>
<p className="text-text-muted">Wypełnij formularz — oddzwonimy i ustalimy termin wizyty</p>
</div>
{/* Pilne zgłoszenie toggle */}
<div
onClick={() => setIsUrgent(!isUrgent)}
className={`flex items-center gap-4 p-4 rounded-2xl border-2 cursor-pointer mb-6 transition-all ${
isUrgent ? 'border-red-400 bg-red-50' : 'border-border bg-white hover:border-red-300'
}`}
>
<div className={`size-10 rounded-xl flex items-center justify-center shrink-0 ${isUrgent ? 'bg-red-500' : 'bg-slate-100'}`}>
<AlertTriangle className={`size-5 ${isUrgent ? 'text-white' : 'text-text-muted'}`} />
</div>
<div className="flex-1">
<p className={`font-bold text-sm ${isUrgent ? 'text-red-700' : 'text-primary'}`}>
{isUrgent ? '🚨 ZGŁOSZENIE PILNE — kontakt w ciągu 2 godzin' : 'Oznacz jako PILNE'}
</p>
<p className="text-xs text-text-muted">Np. upał, brak chłodzenia, awaria krytyczna</p>
</div>
<div className={`size-6 rounded-full border-2 flex items-center justify-center shrink-0 ${isUrgent ? 'border-red-500 bg-red-500' : 'border-slate-300'}`}>
{isUrgent && <Check className="size-3.5 text-white" />}
</div>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
{/* Sekcja 1: Urządzenie */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<Zap className="size-4 text-accent" /> Urządzenie
</h2>
<div className="flex flex-col gap-4">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Marka *</label>
<select
required value={brand} onChange={e => { setBrand(e.target.value); setErrorCode(''); }}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
>
<option value="">Wybierz markę...</option>
{BRANDS.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Model (opcjonalnie)</label>
<input
value={model} onChange={e => setModel(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
placeholder="Np. Gree Pular, LG Artcool 3,5 kW..."
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Na gwarancji?</label>
<select
value={warranty} onChange={e => setWarranty(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
>
<option value="">Nie wiem</option>
<option value="tak">Tak</option>
<option value="nie">Nie</option>
</select>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Ostatni serwis</label>
<select
value={lastService} onChange={e => setLastService(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
>
<option value="">Nie pamiętam</option>
<option value="<1rok">Mniej niż rok temu</option>
<option value="1-2lata">1-2 lata temu</option>
<option value="2-5lat">2-5 lat temu</option>
<option value=">5lat">Ponad 5 lat temu</option>
<option value="nigdy">Nigdy nie był serwisowany</option>
</select>
</div>
</div>
</div>
</div>
{/* Sekcja 2: Problem */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<AlertTriangle className="size-4 text-accent" /> Opis problemu
</h2>
<div className="flex flex-col gap-4">
<div>
<label className="text-xs font-medium text-primary mb-2 block">Rodzaj problemu *</label>
<div className="flex flex-wrap gap-2">
{PROBLEM_TYPES.map(p => (
<button
key={p} type="button"
onClick={() => setProblemType(p)}
className={`px-3 py-1.5 rounded-xl text-xs font-semibold border-2 transition-all ${
problemType === p ? 'border-accent bg-accent text-white' : 'border-border bg-white text-primary hover:border-accent'
}`}
>
{p}
</button>
))}
</div>
</div>
{/* Kod błędu z podpowiedzią */}
<div>
<label className="text-xs font-medium text-primary mb-1 block">
Kod błędu z wyświetlacza (opcjonalnie)
</label>
<input
value={errorCode}
onChange={e => setErrorCode(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
placeholder="Np. E1, F3, CH05..."
/>
{errorHint && (
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-xl text-xs text-blue-700">
<span className="font-bold">Kod {errorCode.toUpperCase()}:</span> {errorHint}
</div>
)}
{errorCode && !errorHint && brand && ERROR_CODES[brand] && (
<p className="mt-1 text-xs text-text-muted">Kodu nie rozpoznano — serwisant zdiagnozuje na miejscu.</p>
)}
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Szczegółowy opis</label>
<textarea
value={description} onChange={e => setDescription(e.target.value)}
rows={3}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none resize-none"
placeholder="Opisz kiedy się pojawił problem, jak często występuje, czy jest jakiś charakterystyczny dźwięk..."
/>
</div>
</div>
</div>
{/* Sekcja 3: Termin */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<Clock className="size-4 text-accent" /> Preferowany termin
</h2>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Data</label>
<input
type="date" value={preferredDate} onChange={e => setPreferredDate(e.target.value)}
min={new Date().toISOString().split('T')[0]}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
/>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Godzina</label>
<select
value={preferredTime} onChange={e => setPreferredTime(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
>
<option value="">Dowolna</option>
<option value="8:00-10:00">8:00 – 10:00</option>
<option value="10:00-12:00">10:00 – 12:00</option>
<option value="12:00-14:00">12:00 – 14:00</option>
<option value="14:00-16:00">14:00 – 16:00</option>
<option value="16:00-18:00">16:00 – 18:00</option>
</select>
</div>
</div>
</div>
{/* Sekcja 4: Dane kontaktowe */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<Phone className="size-4 text-accent" /> Dane kontaktowe
</h2>
<div className="flex flex-col gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Imię i nazwisko *</label>
<input
required value={name} onChange={e => setName(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
placeholder="Jan Kowalski"
/>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Telefon *</label>
<input
required type="tel" value={phone} onChange={e => setPhone(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
placeholder="+48 600 000 000"
/>
</div>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Miasto *</label>
<select
required value={city} onChange={e => setCity(e.target.value)}
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
>
<option value="">Wybierz miasto...</option>
{['Wrocław', 'Legnica', 'Wałbrzych', 'Jelenia Góra', 'Lubin', 'Głogów', 'Świdnica', 'Dzierżoniów', 'Oleśnica', 'Oława', 'Trzebnica', 'Środa Śląska', 'Siechnice', 'Inne'].map(c => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
</div>
</div>
{/* Zgoda RODO */}
<div
className="flex items-start gap-3 cursor-pointer"
onClick={() => setConsent(!consent)}
>
<div className={`size-5 rounded border-2 shrink-0 mt-0.5 flex items-center justify-center transition-colors ${
consent ? 'bg-accent border-accent' : 'bg-white border-slate-300'
}`}>
{consent && <Check className="size-3 text-white" />}
</div>
<span className="text-xs text-text-muted leading-relaxed">
Wyrażam zgodę na przetwarzanie danych osobowych podanych w formularzu
w celu obsługi zgłoszenia serwisowego przez firmę ThermCool. *
</span>
</div>
{/* Submit */}
<button
type="submit"
disabled={isSubmitting || !brand || !problemType || !name || !phone || !city || !consent}
className={`flex items-center justify-center gap-2 py-4 px-6 rounded-2xl font-bold text-base transition-all ${
isUrgent
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-accent hover:bg-accent/90 text-white'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
{isSubmitting ? 'Wysyłanie...' : isUrgent ? '🚨 Wyślij PILNE zgłoszenie' : 'Wyślij zgłoszenie serwisowe'}
</button>
<p className="text-xs text-text-muted text-center">
{isUrgent ? 'Odpowiadamy na pilne zgłoszenia w ciągu 2 godzin w godzinach pracy.' : 'Skontaktujemy się w ciągu 24 godzin.'}
{' '}Możesz też zadzwonić bezpośrednio: <a href={`tel:${companyDetails.phoneRaw}`} className="text-accent font-semibold">{companyDetails.phone}</a>
</p>
</form>
</div>
</main>
);
}
+62 -25
View File
@@ -4,27 +4,27 @@ import React, { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { z } from '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 { submitContactForm } from '@/app/kontakt/actions';
import { companyDetails } from "@/content/company";
const contactFormSchema = z.object({ const contactFormSchema = z.object({
name: z.string().min(2, "Imię musi mieć min. 2 znaki"), name: z.string().min(2, "Imię musi mieć min. 2 znaki"),
phone: z.string().min(9, "Wprowadź poprawny numer telefonu"), phone: z.string().min(9, "Wprowadź poprawny numer telefonu"),
email: z.string().email("Niepoprawny adres e-mail"), email: z.union([z.literal(''), z.string().email("Niepoprawny adres e-mail")]).optional(),
subject: z.string().min(1, "Wybierz temat"), city: z.string().min(1, "Wybierz miasto"),
message: z.string().min(10, "Wiadomość musi mieć min. 10 znaków"), subject: z.string().optional(),
consent: z.boolean().refine((val) => val === true, "Zgoda RODO jest wymagana"), message: z.string().optional(),
}); });
type ContactFormValues = z.infer<typeof contactFormSchema>; type ContactFormValues = z.infer<typeof contactFormSchema>;
export function ContactForm() { export function ContactForm() {
const [serverState, setServerState] = useState<{ success?: boolean; message?: string } | null>(null); const [serverState, setServerState] = useState<{ success?: boolean; message?: string } | null>(null);
const [consent, setConsent] = useState(false);
const { const {
register, register,
handleSubmit, handleSubmit: hookFormSubmit,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
reset reset
} = useForm<ContactFormValues>({ } = useForm<ContactFormValues>({
@@ -33,20 +33,20 @@ export function ContactForm() {
name: '', name: '',
phone: '', phone: '',
email: '', email: '',
city: '',
subject: '', subject: '',
message: '', message: '',
consent: false,
} }
}); });
const onSubmit = async (data: ContactFormValues) => { const onSubmit = async (data: ContactFormValues) => {
setServerState(null); setServerState(null);
// Przekształcamy dane do formatu FormData dla Server Action
const formData = new FormData(); const formData = new FormData();
Object.entries(data).forEach(([key, value]) => { 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); const response = await submitContactForm(null, formData);
@@ -57,9 +57,19 @@ export function ContactForm() {
if (response.success) { if (response.success) {
reset(); 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) { if (serverState?.success) {
return ( return (
<div className="bg-green-50 text-green-800 p-8 rounded-2xl border border-green-200 flex flex-col items-center text-center"> <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 ( 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 && ( {serverState && !serverState.success && (
<div className="bg-red-50 text-red-800 p-4 rounded-xl border border-red-200 flex items-start gap-3"> <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" /> <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"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Imię */} {/* Imię */}
<div className="space-y-2"> <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 <input
{...register("name")} {...register("name")}
id="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`} 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" placeholder="Jan Kowalski"
aria-invalid={errors.name ? "true" : "false"} aria-invalid={errors.name ? "true" : "false"}
@@ -101,11 +112,12 @@ export function ContactForm() {
{/* Telefon */} {/* Telefon */}
<div className="space-y-2"> <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 <input
{...register("phone")} {...register("phone")}
id="phone" id="phone"
type="tel" 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`} 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" placeholder="+48 123 456 789"
/> />
@@ -127,6 +139,34 @@ export function ContactForm() {
{errors.email && <p className="text-red-500 text-xs font-medium">{errors.email.message}</p>} {errors.email && <p className="text-red-500 text-xs font-medium">{errors.email.message}</p>}
</div> </div>
{/* Miasto */}
<div className="space-y-2">
<label htmlFor="city" className="text-sm font-semibold text-primary">Miasto <span className="text-red-500">*</span></label>
<select
{...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 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.city && <p className="text-red-500 text-xs font-medium">{errors.city.message}</p>}
</div>
</div>
{/* Temat */} {/* Temat */}
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="subject" className="text-sm font-semibold text-primary">Temat rozmowy</label> <label htmlFor="subject" className="text-sm font-semibold text-primary">Temat rozmowy</label>
@@ -145,7 +185,6 @@ export function ContactForm() {
</select> </select>
{errors.subject && <p className="text-red-500 text-xs font-medium">{errors.subject.message}</p>} {errors.subject && <p className="text-red-500 text-xs font-medium">{errors.subject.message}</p>}
</div> </div>
</div>
{/* Wiadomość */} {/* Wiadomość */}
<div className="space-y-2"> <div className="space-y-2">
@@ -162,19 +201,17 @@ export function ContactForm() {
{/* RODO */} {/* RODO */}
<div className="space-y-2"> <div className="space-y-2">
<label className="flex items-start gap-3 cursor-pointer group"> <div className="flex items-start gap-3 cursor-pointer" onClick={() => setConsent(!consent)}>
<div className="relative flex items-start pt-1"> <div className={`size-5 rounded border-2 shrink-0 mt-0.5 flex items-center justify-center transition-colors ${
<input consent ? 'bg-accent border-accent' : 'bg-white border-slate-300'
type="checkbox" }`}>
{...register("consent")} {consent && <Check className="size-3 text-white" />}
className="w-5 h-5 rounded border-border text-primary focus:ring-primary focus:ring-offset-0 transition-colors"
/>
</div> </div>
<span className="text-xs text-text-muted leading-relaxed select-none"> <input type="hidden" name="consent" value={consent ? 'true' : ''} />
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. <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> </span>
</label> </div>
{errors.consent && <p className="text-red-500 text-xs font-medium">{errors.consent.message}</p>}
</div> </div>
<button <button
+260 -37
View File
@@ -38,33 +38,49 @@ function estimateInstall(pipes: string, rooms: number): { min: number; max: numb
return { min: base + pipeExtra, max: base + pipeExtra + 600 }; 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) { 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 [step, setStep] = useState(0);
const [building, setBuilding] = useState('apartment'); const [building, setBuilding] = useState('apartment');
const [m2, setM2] = useState(25); const [m2, setM2] = useState(25);
const [floor, setFloor] = useState('middle'); // ground | middle | top const [floor, setFloor] = useState('middle'); // ground | middle | top
const [sun, setSun] = useState('south'); // north | east | south | west const [sun, setSun] = useState('south'); // north | east | south | west
const [systemType, setSystemType] = useState<'auto' | 'scienne' | 'multisplit'>('auto');
const [rooms, setRooms] = useState(1); const [rooms, setRooms] = useState(1);
const [pipes, setPipes] = useState('medium'); // short(<3m) | medium(3-6m) | long(6-10m) | verylong(>10m) const [pipes, setPipes] = useState('medium'); // short(<3m) | medium(3-6m) | long(6-10m) | verylong(>10m)
const [budget, setBudget] = useState(8000); const [budget, setBudget] = useState(8000);
const [showResults, setShowResults] = useState(false); const [showResults, setShowResults] = useState(false);
const [preferredBrand, setPreferredBrand] = useState('');
const requiredPower = useMemo(() => calculatePower(m2, floor, sun, building), [m2, floor, sun, building]); const requiredPower = useMemo(() => calculatePower(m2, floor, sun, building), [m2, floor, sun, building]);
const installCost = useMemo(() => estimateInstall(pipes, rooms), [pipes, rooms]); 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(() => { const results = useMemo(() => {
if (!showResults) return []; if (!showResults || deviceType === 'pompa') return [];
const powerMin = requiredPower * 0.8;
const powerMax = requiredPower * 1.4;
const maxPrice = budget - installCost.min;
return products const maxPrice = budget * 0.75;
const powerMin = requiredPower * 0.6;
const powerMax = requiredPower * 1.8;
const filtered = products
.filter(p => { .filter(p => {
if (p.category !== category) return false; if (p.category !== category) 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; if (p.price === null || p.price > maxPrice) return false;
}
const moc = parseFloat(String(p.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]); const moc = parseFloat(String(p.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]);
return moc >= powerMin && moc <= powerMax; 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 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]); const mocB = parseFloat(String(b.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]);
return Math.abs(mocA - requiredPower) - Math.abs(mocB - requiredPower); return Math.abs(mocA - requiredPower) - Math.abs(mocB - requiredPower);
}) });
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); .slice(0, 6);
}, [showResults, products, requiredPower, category, budget, installCost.min]); }, [showResults, deviceType, products, requiredPower, category, budget, preferredBrand]);
const handleFinish = () => setShowResults(true); const handleFinish = () => setShowResults(true);
const handleReset = () => { setShowResults(false); setStep(0); }; 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 (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 ( return (
<main className="bg-slate-50 min-h-screen py-12"> <main className="bg-slate-50 min-h-screen py-12">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8"> <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: 'Wymagana moc', value: `${requiredPower} kW` },
{ label: 'Powierzchnia', value: `${m2} m²` }, { 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ł` }, { label: 'Szac. montaż', value: `${installCost.min.toLocaleString('pl-PL')}–${installCost.max.toLocaleString('pl-PL')} zł` },
].map((s, i) => ( ].map((s, i) => (
<div key={i} className="bg-slate-50 rounded-xl p-3"> <div key={i} className="bg-slate-50 rounded-xl p-3">
@@ -106,7 +166,15 @@ export function KalkulatorClient({ products }: Props) {
{results.length > 0 ? ( {results.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
{results.map(product => ( {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 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"> <div className="relative aspect-[4/3] bg-slate-50">
<Image <Image
@@ -122,12 +190,12 @@ export function KalkulatorClient({ products }: Props) {
</span> </span>
)} )}
</div> </div>
<div className="p-4"> <div className="p-4 flex flex-col h-full">
<p className="text-xs text-accent font-bold uppercase mb-1">{product.brand}</p> <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"> <h3 className="text-sm font-bold text-primary font-heading leading-snug mb-3 line-clamp-2">
{product.name} {product.name}
</h3> </h3>
<div className="flex flex-wrap gap-1.5 mb-3"> <div className="flex flex-wrap gap-1.5 mb-1">
{product.specs?.['Moc chłodnicza'] && ( {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"> <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 {product.specs['Moc chłodnicza']} kW
@@ -139,7 +207,12 @@ export function KalkulatorClient({ products }: Props) {
</span> </span>
)} )}
</div> </div>
<div className="flex items-end justify-between">
<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> <div>
<p className="text-lg font-extrabold text-primary font-heading leading-none"> <p className="text-lg font-extrabold text-primary font-heading leading-none">
{product.price?.toLocaleString('pl-PL')} zł {product.price?.toLocaleString('pl-PL')} zł
@@ -154,7 +227,8 @@ export function KalkulatorClient({ products }: Props) {
</div> </div>
</div> </div>
</div> </div>
))} );
})}
</div> </div>
) : ( ) : (
<div className="bg-white rounded-2xl border border-border p-12 text-center"> <div className="bg-white rounded-2xl border border-border p-12 text-center">
@@ -169,6 +243,31 @@ export function KalkulatorClient({ products }: Props) {
</div> </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"> <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"> <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 <RotateCcw className="size-4" /> Zacznij od nowa
@@ -190,9 +289,9 @@ export function KalkulatorClient({ products }: Props) {
{/* Header */} {/* Header */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<h1 className="text-3xl font-extrabold text-primary font-heading mb-2"> <h1 className="text-3xl font-extrabold text-primary font-heading mb-2">
Dobierz klimatyzator Dobierz urządzenie
</h1> </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> </div>
{/* Pasek postępu */} {/* Pasek postępu */}
@@ -213,8 +312,90 @@ export function KalkulatorClient({ products }: Props) {
{/* Karty kroków */} {/* Karty kroków */}
<div className="bg-white rounded-2xl border border-border p-6 md:p-8"> <div className="bg-white rounded-2xl border border-border p-6 md:p-8">
{/* Krok 0: Pomieszczenie */} {/* Krok 0: Typ urządzenia */}
{step === 0 && ( {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 flex-col gap-6">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<div className="size-10 rounded-xl bg-blue-50 flex items-center justify-center"> <div className="size-10 rounded-xl bg-blue-50 flex items-center justify-center">
@@ -316,25 +497,43 @@ export function KalkulatorClient({ products }: Props) {
</div> </div>
)} )}
{/* Krok 1: Konfiguracja */} {/* Krok 2: Konfiguracja */}
{step === 1 && ( {step === 2 && (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<div className="size-10 rounded-xl bg-purple-50 flex items-center justify-center"> <div className="size-10 rounded-xl bg-purple-50 flex items-center justify-center">
<Thermometer className="size-5 text-purple-600" /> <Thermometer className="size-5 text-purple-600" />
</div> </div>
<div> <div>
<h2 className="text-lg font-bold text-primary font-heading">Ile pomieszczeń?</h2> <h2 className="text-lg font-bold text-primary font-heading">Konfiguracja</h2>
<p className="text-xs text-text-muted">1 pomieszczenie = split, więcej = multisplit</p> <p className="text-xs text-text-muted">Ilość pomieszczeń i typ instalacji</p>
</div> </div>
</div> </div>
<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"> <div className="grid grid-cols-2 gap-3">
{[ {[
{ value: 1, label: '1 pomieszczenie', sub: 'Klimatyzator split', icon: '🏠' }, { value: 1, label: '1 pomieszczenie', sub: 'Pojedynczy pokój', icon: '🏠' },
{ value: 2, label: '2 pomieszczenia', sub: 'System multisplit 2+1', icon: '🏡' }, { value: 2, label: '2 pomieszczenia', sub: 'Dwa pokoje', icon: '🏡' },
{ value: 3, label: '3 pomieszczenia', sub: 'System multisplit 3+1', icon: '🏘️' }, { value: 3, label: '3 pomieszczenia', sub: 'Trzy pokoje', icon: '🏘️' },
{ value: 4, label: '4-5 pomieszczeń', sub: 'System multisplit 4+1', icon: '🏗️' }, { value: 4, label: '4-5 pomieszczeń', sub: 'Wiele pokoi', icon: '🏗️' },
].map(o => ( ].map(o => (
<button key={o.value} onClick={() => setRooms(o.value)} <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'}`}> className={`p-4 rounded-xl border-2 text-left transition-all ${rooms === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
@@ -344,17 +543,18 @@ export function KalkulatorClient({ products }: Props) {
</button> </button>
))} ))}
</div> </div>
</div>
<div className="p-3 bg-slate-50 rounded-xl text-xs text-text-muted"> <div className="p-3 bg-slate-50 rounded-xl text-xs text-text-muted">
{rooms === 1 {category === 'scienne'
? '✅ Klimatyzator split — jedna jednostka wewnętrzna + jedna zewnętrzna. Idealne do pojedynczego pomieszczenia.' ? '✅ Wybrano: Klimatyzator split — jedna jednostka wewnętrzna i jedna zewnętrzna. Dedykowane dla jednego pokoju.'
: `✅ System multisplit — jedna jednostka zewnętrzna obsługuje ${rooms} jednostki wewnętrzne. Ekonomiczne i estetyczne rozwiązanie dla całego domu.`} : `✅ Wybrano: System multisplit — jedna jednostka zewnętrzna obsłuży ${rooms} jednostki wewnętrzne. Lepsza estetyka na zewnątrz budynku.`}
</div> </div>
</div> </div>
)} )}
{/* Krok 2: Instalacja */} {/* Krok 3: Instalacja */}
{step === 2 && ( {step === 3 && (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<div className="size-10 rounded-xl bg-orange-50 flex items-center justify-center"> <div className="size-10 rounded-xl bg-orange-50 flex items-center justify-center">
@@ -388,8 +588,8 @@ export function KalkulatorClient({ products }: Props) {
</div> </div>
)} )}
{/* Krok 3: Budżet */} {/* Krok 4: Budżet */}
{step === 3 && ( {step === 4 && (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<div className="size-10 rounded-xl bg-green-50 flex items-center justify-center"> <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"> <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-xs text-text-muted mb-1">Na urządzenie zostaje</p>
<p className="text-base font-bold text-primary font-heading"> <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> </p>
</div> </div>
<div className="bg-orange-50 rounded-xl p-3"> <div className="bg-orange-50 rounded-xl p-3">
@@ -432,7 +632,28 @@ export function KalkulatorClient({ products }: Props) {
</div> </div>
<div className="p-3 bg-blue-50 rounded-xl text-xs text-blue-700"> <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>
</div> </div>
)} )}
@@ -446,11 +667,13 @@ export function KalkulatorClient({ products }: Props) {
</button> </button>
) : <div />} ) : <div />}
{step < 3 ? ( {step < STEPS.length - 1 ? (
<button onClick={() => setStep(s => s + 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"> 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" /> Dalej <ArrowRight className="size-4" />
</button> </button>
)
) : ( ) : (
<button onClick={handleFinish} <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"> className="flex items-center gap-2 px-6 py-2.5 bg-accent text-white rounded-xl text-sm font-bold hover:bg-accent/90 transition-colors">
+1 -1
View File
@@ -135,7 +135,7 @@ export function Footer() {
</Link> </Link>
</li> </li>
<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 Zgłoś awarię / serwis
</Link> </Link>
</li> </li>
+18 -10
View File
@@ -2,7 +2,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Link from "next/link"; 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 { NavDropdown } from "./NavDropdown";
import { GlobalSearch } from "./GlobalSearch"; import { GlobalSearch } from "./GlobalSearch";
import { MobileMenu } from "./MobileMenu"; import { MobileMenu } from "./MobileMenu";
@@ -164,24 +164,24 @@ export function Header() {
sections={acSections} sections={acSections}
align="left" align="left"
/> />
<NavDropdown
label="Miasta"
sections={miastaSections}
align="left"
/>
<Link <Link
href="/pompy-ciepla" href="/pompy-ciepla"
className="text-sm font-medium text-primary hover:text-accent transition-colors" className="text-sm font-medium text-primary hover:text-accent transition-colors"
> >
Pompy ciepła Pompy ciepła
</Link> </Link>
<NavDropdown
label="Miasta"
sections={miastaSections}
align="left"
/>
<Link <Link
href="/realizacje" href="/kalkulator"
className="text-sm font-medium text-primary hover:text-accent transition-colors" className="text-sm font-medium text-primary hover:text-accent transition-colors"
> >
Realizacje Dobór urządzenia
</Link> </Link>
<Link <Link
href="/blog" href="/blog"
className="text-sm font-medium text-primary hover:text-accent transition-colors" className="text-sm font-medium text-primary hover:text-accent transition-colors"
@@ -203,12 +203,20 @@ export function Header() {
</nav> </nav>
{/* CTA Button */} {/* CTA Button */}
<Link href="/kontakt" className="hidden lg:flex"> <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"> <button className="btn btn-primary font-bold">
Darmowa wycena Darmowa wycena
</button> </button>
</Link> </Link>
</div> </div>
</div>
</header> </header>
{/* ----------------- MOBILE: HEADER (STICKY) ----------------- */} {/* ----------------- MOBILE: HEADER (STICKY) ----------------- */}
+25 -14
View File
@@ -100,6 +100,17 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
</div> </div>
</details> </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 */} {/* Nowa sekcja: Miasta */}
<div className="border-b border-border py-3"> <div className="border-b border-border py-3">
<details className="collapse collapse-arrow rounded-none"> <details className="collapse collapse-arrow rounded-none">
@@ -136,30 +147,19 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
</details> </details>
</div> </div>
{/* 2. Pompy Ciepła */} {/* 3. Dobór urządzenia */}
<div className="border-b border-border py-3"> <div className="border-b border-border py-3">
<Link <Link
href="/pompy-ciepla" href="/kalkulator"
onClick={onClose} onClick={onClose}
className="text-sm font-medium text-primary hover:text-accent block" className="text-sm font-medium text-primary hover:text-accent block"
> >
Pompy ciepła Dobór urządzenia
</Link> </Link>
</div> </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 */} {/* 5. Blog */}
<div className="border-b border-border py-3"> <div className="border-b border-border py-3">
<Link <Link
@@ -192,6 +192,17 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
Kontakt Kontakt
</Link> </Link>
</div> </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>
</div> </div>
+15 -15
View File
@@ -1,26 +1,26 @@
import { Clock, ShieldCheck, Shield, Percent, Star } from "lucide-react"; import { Wrench, CheckCircle, Clock, Star } from "lucide-react";
export function TrustBar() { export function TrustBar() {
const items = [ 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, icon: Clock,
title: "Szybki termin realizacji", title: "Bezpłatna wycena",
description: "Ekspresowy montaż i serwis", description: "Odpowiadamy w ciągu 24h",
},
{
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",
}, },
{ {
icon: Star, icon: Star,
title: "Opinie Google 5.0", title: "Estetyczny montaż",
description: "Zaufanie setek klientów", description: "Schludna instalacja, bez bałaganu",
}, },
]; ];
+2 -2
View File
@@ -2,9 +2,9 @@ import { CheckCircle2, Zap, ShieldCheck, Banknote } from "lucide-react";
export function AdvantagesBar() { export function AdvantagesBar() {
const advantages = [ const advantages = [
{ icon: Zap, text: "Szybki termin realizacji" }, { icon: Zap, text: "Certyfikat F-Gaz" },
{ icon: ShieldCheck, text: "Gwarancja na sprzęt i montaż" }, { icon: ShieldCheck, text: "Gwarancja na sprzęt i montaż" },
{ icon: CheckCircle2, text: "Uprawnienia F-gaz" }, { icon: CheckCircle2, text: "Bezpłatna wycena w 24h" },
]; ];
return ( return (
+6 -25
View File
@@ -15,14 +15,14 @@ export interface HeroSectionProps {
} }
export function HeroSection({ export function HeroSection({
eyebrow = "Klimatyzacja i Pompy Ciepła", eyebrow = "Wrocław i Dolny Śląsk",
title = ( title = (
<> <>
Klimatyzacja, pompy ciepła<br className="hidden sm:inline" /> <span className="text-white">Klimatyzacja i pompy ciepła</span><br />
<span className="text-accent"> i usługi domowe</span> <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", ctaText = "Darmowa wycena",
ctaLink = "/kontakt", ctaLink = "/kontakt",
secondaryCtaText = "Zobacz ofertę", secondaryCtaText = "Zobacz ofertę",
@@ -87,29 +87,10 @@ export function HeroSection({
</button> </button>
</Link> </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> </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>
</div> </div>
</section> </section>
+63 -42
View File
@@ -1,57 +1,78 @@
import { Star, Quote } from "lucide-react"; 'use client';
import { SectionHeading } from "../common/SectionHeading";
import { testimonials } from "@/content/testimonials"; 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() { 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 ( 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"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading <div className="flex items-center justify-between mb-10">
eyebrow="Opinie Klientów" <div>
title="Co mówią o nas nasi klienci?" <h2 className="text-2xl md:text-3xl font-extrabold text-primary font-heading">
subtitle="Zadowolenie klientów to nasz najwyższy cel. Zobacz opinie osób, które zaufały naszym montażystom i serwisantom." Co mówią nasi klienci
align="center" </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="overflow-hidden" ref={emblaRef}>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="flex gap-5">
{testimonials.map((test, idx) => ( {testimonials.map((t, i) => (
<div <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">
key={idx} <div className="flex gap-0.5">
className="relative p-8 rounded-xl bg-white border border-border hover:shadow-md transition-shadow flex flex-col justify-between" {Array.from({ length: t.rating }).map((_, j) => (
> <Star key={j} className="size-4 fill-yellow-400 text-yellow-400" />
{/* 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> </div>
<p className="text-sm text-text-muted leading-relaxed flex-1">"{t.text}"</p>
{/* Review Text */} <div className="pt-3 border-t border-border">
<p className="text-sm md:text-base text-primary italic leading-relaxed"> <p className="text-sm font-bold text-primary">{t.name}</p>
&ldquo;{test.text}&rdquo; <p className="text-xs text-text-muted">{t.city}</p>
</p>
</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> </div>
</div> </div>
</div>
</section> </section>
); );
} }
+8 -6
View File
@@ -76,21 +76,23 @@ export function ProductCard({ product }: ProductCardProps) {
unoptimized={product.image.startsWith('http')} unoptimized={product.image.startsWith('http')}
/> />
{/* Badge marka */} {/* Brand Logo or Name */}
<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]"> <div className="absolute top-3 left-3 z-10 flex flex-col gap-1.5">
{brandLogos[product.brand] ? ( {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 <Image
src={brandLogos[product.brand]} src={brandLogos[product.brand]}
alt={product.brand} alt={product.brand}
width={56} width={40}
height={20} height={14}
className="object-contain" className="object-contain"
unoptimized unoptimized
/> />
</div> </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> </div>
+115 -87
View File
@@ -11,7 +11,6 @@ import { Product } from '@/content/products';
import { useCartStore } from '@/lib/cart-store'; import { useCartStore } from '@/lib/cart-store';
import { useCompareStore } from '@/lib/compare-store'; import { useCompareStore } from '@/lib/compare-store';
import { getProductVariants } from '@/content/product-variants'; import { getProductVariants } from '@/content/product-variants';
import { useRouter } from 'next/navigation';
const brandLogos: Record<string, string> = { const brandLogos: Record<string, string> = {
'Gree': '/brands/gree.webp', 'Gree': '/brands/gree.webp',
@@ -29,6 +28,7 @@ const brandLogos: Record<string, string> = {
interface Props { interface Props {
product: Product; product: Product;
seriesProducts: Product[];
companyPhone: string; companyPhone: string;
companyPhoneRaw: string; companyPhoneRaw: string;
} }
@@ -55,48 +55,54 @@ function parseMoc(val: string): number {
return floats[Math.floor(floats.length / 2)]; 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]; 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 [activeTab, setActiveTab] = useState<Tab>('Opis');
const [godziny, setGodziny] = useState(8); const [godziny, setGodziny] = useState(8);
const [added, setAdded] = useState(false); const [cartState, setCartState] = useState<'idle' | 'added' | 'cart'>('idle');
const router = useRouter(); const variants = getProductVariants(currentProduct.slug);
const variants = getProductVariants(product.slug);
const addToCart = useCartStore(s => s.addItem); const addToCart = useCartStore(s => s.addItem);
const { addItem: addCompare, removeItem: removeCompare, isAdded } = useCompareStore(); const { addItem: addCompare, removeItem: removeCompare, isAdded } = useCompareStore();
const inCompare = isAdded(product.slug); const inCompare = isAdded(currentProduct.slug);
const handleAddToCart = () => { const handleAddToCart = () => {
if (!product.price) return; if (!currentProduct.price) return;
addToCart({ slug: product.slug, name: product.name, brand: product.brand, image: product.image, price: product.price }); addToCart({
setAdded(true); slug: currentProduct.slug,
setTimeout(() => setAdded(false), 1500); name: currentProduct.name,
brand: currentProduct.brand,
image: currentProduct.image,
price: currentProduct.price,
});
setCartState('added');
setTimeout(() => setCartState('cart'), 1500);
}; };
const handleCompare = () => { const handleCompare = () => {
if (inCompare) removeCompare(product.slug); if (inCompare) removeCompare(currentProduct.slug);
else addCompare({ slug: product.slug, name: product.name, brand: product.brand, price: product.price, image: product.image, specs: product.specs }); else addCompare({ slug: currentProduct.slug, name: currentProduct.name, brand: currentProduct.brand, price: currentProduct.price, image: currentProduct.image, specs: currentProduct.specs });
}; };
// Kalkulator // Kalkulator
const mocChlodnicza = parseMoc(product.specs['Moc chłodnicza'] || ''); const mocChlodnicza = parseMoc(currentProduct.specs['Moc chłodnicza'] || '');
const pobórMocy = parseMoc(product.specs['Pobór mocy chłodzenie (kW)'] || '') || (mocChlodnicza / estimateSEER(product.specs['Klasa energetyczna'] || '')); const pobórMocy = parseMoc(currentProduct.specs['Pobór mocy chłodzenie (kW)'] || '') || (mocChlodnicza / estimateSEER(currentProduct.specs['Klasa energetyczna'] || ''));
const cenaKwh = 0.75; const cenaKwh = 0.75;
const kosztGodzina = pobórMocy * cenaKwh; const kosztGodzina = pobórMocy * cenaKwh;
const kosztMiesiac = kosztGodzina * godziny * 30; const kosztMiesiac = kosztGodzina * godziny * 30;
// Kluczowe parametry do ikon // Kluczowe parametry do ikon
const params = [ const params = [
product.specs['Klasa energetyczna'] && { icon: Star, label: 'Klasa energetyczna', value: product.specs['Klasa energetyczna'], color: 'text-green-600 bg-green-50' }, currentProduct.specs['Klasa energetyczna'] && { icon: Star, label: 'Klasa energetyczna', value: currentProduct.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' }, 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' },
product.specs['Moc grzewcza (kW)'] && { icon: Thermometer, label: 'Moc grzewcza', value: `${product.specs['Moc grzewcza (kW)']} kW`, color: 'text-orange-600 bg-orange-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' },
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' }, 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' },
product.specs['Czynnik chłodniczy'] && { icon: Wind, label: 'Czynnik', value: product.specs['Czynnik chłodniczy'], color: 'text-teal-600 bg-teal-50' }, currentProduct.specs['Czynnik chłodniczy'] && { icon: Wind, label: 'Czynnik', value: currentProduct.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['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 }[]; ].filter(Boolean) as { icon: any; label: string; value: string; color: string }[];
return ( return (
@@ -109,31 +115,31 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
<span>/</span> <span>/</span>
<Link href="/sklep" className="hover:text-accent transition-colors">Sklep</Link> <Link href="/sklep" className="hover:text-accent transition-colors">Sklep</Link>
<span>/</span> <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>/</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> </nav>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 xl:gap-12"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 xl:gap-12">
{/* LEWA: Zdjęcie */} {/* LEWA: Zdjęcie */}
<div> <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 <Image
src={product.image} src={currentProduct.image}
alt={product.name} alt={currentProduct.name}
fill fill
className="object-contain p-8" className="object-contain p-6"
priority priority
unoptimized={product.image.startsWith('http')} unoptimized={currentProduct.image.startsWith('http')}
/> />
{/* Badges */} {/* 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"> <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> </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"> <div className="absolute top-4 right-4 px-2.5 py-1 bg-accent text-white text-xs font-bold rounded-lg shadow">
MULTISPLIT MULTISPLIT
</div> </div>
@@ -142,36 +148,45 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
</div> </div>
{/* PRAWA: Info */} {/* PRAWA: Info */}
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-3">
{/* Nagłówek */} {/* Nagłówek */}
<div> <div>
<div className="mb-2"> <div className="mb-2 flex items-center gap-3 flex-wrap">
{brandLogos[product.brand] ? ( {brandLogos[currentProduct.brand] ? (
<div className="bg-white rounded-lg p-2 inline-block border border-border"> <div className="bg-white rounded-lg p-1.5 inline-block border border-border">
<Image <Image
src={brandLogos[product.brand]} src={brandLogos[currentProduct.brand]}
alt={product.brand} alt={currentProduct.brand}
width={120} width={80}
height={36} height={28}
className="object-contain object-left" className="object-contain object-left"
unoptimized unoptimized
/> />
</div> </div>
) : ( ) : (
<span className="text-xs font-bold text-accent uppercase tracking-widest"> <span className="text-xs font-bold text-accent uppercase tracking-widest">
{product.brand} {currentProduct.brand}
</span> </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> </div>
<h1 className="text-2xl md:text-3xl font-extrabold text-primary font-heading leading-tight"> <h1 className="text-2xl md:text-3xl font-extrabold text-primary font-heading leading-tight">
{product.name} {currentProduct.name}
</h1> </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> </div>
{/* Warianty mocy */} {/* Warianty mocy */}
{variants && variants.moce.length > 0 && ( {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"> <p className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
Dostępne moce (kW): Dostępne moce (kW):
</p> </p>
@@ -179,20 +194,26 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{variants.moce.map((v) => ( {variants.moce.map((v) => (
<button <button
key={v.slug} key={v.slug}
onClick={() => router.push(`/sklep/${v.slug}`)} onClick={() => {
className={`px-4 py-2 rounded-xl text-sm font-bold border-2 transition-all ${ const p = seriesProducts.find(sp => sp.slug === v.slug);
v.slug === product.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-accent bg-accent text-white shadow-sm'
: 'border-border bg-white text-primary hover:border-accent hover:text-accent' : 'border-border bg-white text-primary hover:border-accent hover:text-accent'
}`} }`}
> >
{v.moc} kW {v.moc} kW
{v.slug !== product.slug && ( {v.slug !== currentProduct.slug && (
<span className="block text-[10px] font-normal opacity-70 mt-0.5"> <span className="block text-[10px] font-normal opacity-70 mt-0.5">
{v.cena.toLocaleString('pl-PL')} zł {v.cena.toLocaleString('pl-PL')} zł
</span> </span>
)} )}
{v.slug === product.slug && ( {v.slug === currentProduct.slug && (
<span className="block text-[10px] font-normal opacity-80 mt-0.5"> <span className="block text-[10px] font-normal opacity-80 mt-0.5">
{v.cena.toLocaleString('pl-PL')} zł {v.cena.toLocaleString('pl-PL')} zł
</span> </span>
@@ -205,7 +226,7 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{/* Warianty wykończenia */} {/* Warianty wykończenia */}
{variants && variants.wykończenia.length > 0 && ( {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"> <p className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
Wykończenie: Wykończenie:
</p> </p>
@@ -213,9 +234,15 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{variants.wykończenia.map((v) => ( {variants.wykończenia.map((v) => (
<button <button
key={v.slug} key={v.slug}
onClick={() => router.push(`/sklep/${v.slug}`)} onClick={() => {
className={`px-4 py-2 rounded-xl text-sm font-bold border-2 transition-all ${ const p = seriesProducts.find(sp => sp.slug === v.slug);
v.slug === product.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-accent bg-accent text-white shadow-sm'
: 'border-border bg-white text-primary hover:border-accent hover:text-accent' : '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) => { {params.map((p, i) => {
const Icon = p.icon; const Icon = p.icon;
return ( return (
<div key={i} className={`flex items-center gap-2 px-3 py-2 rounded-xl ${p.color} border border-current/10`}> <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-4 shrink-0" /> <Icon className="size-3.5 shrink-0" />
<div className="min-w-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-[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>
</div> </div>
); );
@@ -246,28 +273,43 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
)} )}
{/* Cena i CTA */} {/* Cena i CTA */}
<div className="bg-white rounded-2xl border border-border p-5"> <div className="bg-white rounded-2xl border border-border p-4">
{product.price !== null ? ( {currentProduct.price !== null ? (
<> <>
<div className="flex items-end gap-3 mb-1"> <div className="flex items-end gap-3 mb-1">
<span className="text-3xl font-extrabold text-primary font-heading"> <span className="text-2xl font-extrabold text-primary font-heading">
{product.price.toLocaleString('pl-PL')} zł {currentProduct.price.toLocaleString('pl-PL')} zł
</span> </span>
</div> </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-1">cena urządzenia brutto (23% VAT)</p>
<p className="text-xs text-text-muted mb-4">Montaż wyceniamy indywidualnie — zadzwoń lub napisz</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"> <div className="flex flex-col sm:flex-row gap-3">
{cartState === 'idle' && (
<button <button
onClick={handleAddToCart} onClick={handleAddToCart}
className={`flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-xl font-bold text-sm transition-all ${ 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"
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</>} <ShoppingCart className="size-4" /> Do koszyka
</button> </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"> <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ż Zapytaj o montaż
</button> </button>
</Link> </Link>
@@ -297,9 +339,9 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{/* Profesjonalna porada */} {/* Profesjonalna porada */}
<a <a
href={`tel:${companyPhoneRaw}`} 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"> <span className="flex items-center gap-2 font-bold">
<Phone className="size-4" /> {companyPhone} <Phone className="size-4" /> {companyPhone}
</span> </span>
@@ -307,7 +349,7 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{/* Kalkulator kosztów energii */} {/* Kalkulator kosztów energii */}
{mocChlodnicza > 0 && ( {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"> <div className="flex items-center gap-2 mb-4">
<Zap className="size-4 text-accent" /> <Zap className="size-4 text-accent" />
<h3 className="text-sm font-bold text-primary font-heading">Kalkulator kosztów energii</h3> <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> </div>
<p className="text-[10px] text-text-muted mt-2 text-center"> <p className="text-[10px] text-text-muted mt-2 text-center">
Szacunek przy {cenaKwh} zł/kWh · {godziny}h/dzień · 30 dni 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> </p>
</div> </div>
)} )}
@@ -376,8 +418,8 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{/* Tab: Opis */} {/* Tab: Opis */}
{activeTab === 'Opis' && ( {activeTab === 'Opis' && (
<div className="max-w-3xl"> <div className="max-w-3xl">
{product.shortDescription ? ( {currentProduct.shortDescription ? (
<p className="text-text-muted leading-relaxed text-base">{product.shortDescription}</p> <p className="text-text-muted leading-relaxed text-base">{currentProduct.shortDescription}</p>
) : ( ) : (
<p className="text-text-muted">Brak opisu produktu.</p> <p className="text-text-muted">Brak opisu produktu.</p>
)} )}
@@ -387,11 +429,11 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
{/* Tab: Specyfikacja */} {/* Tab: Specyfikacja */}
{activeTab === 'Specyfikacja' && ( {activeTab === 'Specyfikacja' && (
<div className="max-w-2xl"> <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"> <div className="rounded-xl border border-border overflow-hidden">
<table className="w-full text-sm"> <table className="w-full text-sm">
<tbody> <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'}> <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-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> <td className="px-5 py-3 text-primary font-semibold">{val}</td>
@@ -430,21 +472,7 @@ export function ProductPageClient({ product, companyPhone, companyPhoneRaw }: Pr
</div> </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>
</div> </div>
</main> </main>
+39
View File
@@ -15,6 +15,8 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"daisyui": "^5.5.1-beta.2", "daisyui": "^5.5.1-beta.2",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"next": "16.2.9", "next": "16.2.9",
"react": "19.2.4", "react": "19.2.4",
@@ -4289,6 +4291,43 @@
"integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==",
"license": "ISC" "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": { "node_modules/emoji-regex": {
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+2
View File
@@ -16,6 +16,8 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"daisyui": "^5.5.1-beta.2", "daisyui": "^5.5.1-beta.2",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"next": "16.2.9", "next": "16.2.9",
"react": "19.2.4", "react": "19.2.4",