feat: Zaktualizuj sekcję hero, nawigację, kalkulator i formularz zgłoszenia awarii
This commit is contained in:
@@ -10,9 +10,10 @@ const resend = new Resend(process.env.RESEND_API_KEY);
|
|||||||
const contactFormSchema = z.object({
|
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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Check, Phone, Wrench, AlertTriangle, ArrowRight } from 'lucide-react';
|
||||||
|
import { companyDetails } from '@/content/company';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Serwis klimatyzacji LG Wrocław — naprawa i przegląd | ThermCool',
|
||||||
|
description: 'Serwisujemy klimatyzatory LG na Dolnym Śląsku. Naprawa, przegląd, odgrzybianie — tam gdzie inne firmy odmawiają. Zadzwoń i umów wizytę.',
|
||||||
|
alternates: { canonical: '/serwis-lg' },
|
||||||
|
openGraph: {
|
||||||
|
title: 'Serwis klimatyzacji LG Wrocław | ThermCool',
|
||||||
|
description: 'Specjalizujemy się w serwisie klimatyzatorów LG. Naprawa, diagnostyka, przegląd na Dolnym Śląsku.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const faqItems = [
|
||||||
|
{
|
||||||
|
q: 'Dlaczego inne firmy odmawiają serwisu klimatyzacji LG?',
|
||||||
|
a: 'Klimatyzatory LG wymagają specjalistycznej wiedzy i dedykowanych narzędzi diagnostycznych. Wiele firm unika tych urządzeń ze względu na złożoność układów elektronicznych i trudniejszy dostęp do części zamiennych. My posiadamy odpowiednie doświadczenie i narzędzia.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Ile kosztuje przegląd klimatyzatora LG?',
|
||||||
|
a: 'Koszt przeglądu klimatyzatora LG zaczyna się od 250 zł i obejmuje czyszczenie filtrów, sprawdzenie ciśnienia czynnika chłodniczego, kontrolę układu elektrycznego i test działania. Dokładna wycena zależy od modelu i stanu urządzenia.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Jak długo trwa naprawa klimatyzatora LG?',
|
||||||
|
a: 'Standardowy przegląd i czyszczenie to 1-2 godziny. Naprawa usterek zależy od rodzaju problemu — prostsze naprawy wykonujemy podczas jednej wizyty, przy bardziej skomplikowanych usterkach czas realizacji ustalamy indywidualnie po diagnostyce.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Czy przyjeżdżacie poza Wrocław?',
|
||||||
|
a: 'Tak, obsługujemy cały Dolny Śląsk — Legnicę, Wałbrzych, Jelenią Górę, Lubin, Świdnicę i okoliczne miejscowości. Koszt dojazdu ustalamy indywidualnie.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Czy naprawiacie wszystkie modele LG?',
|
||||||
|
a: 'Serwisujemy większość klimatyzatorów LG dostępnych na rynku, w tym modele z serii Artcool, Standard Plus, Dualcool i MultiSplit. W przypadku bardzo starych lub rzadkich modeli skontaktuj się z nami przed umówieniem wizyty.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function SerwisLGPage() {
|
||||||
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Service',
|
||||||
|
name: 'Serwis klimatyzacji LG',
|
||||||
|
provider: {
|
||||||
|
'@type': 'HVACBusiness',
|
||||||
|
name: companyDetails.name,
|
||||||
|
telephone: companyDetails.phone,
|
||||||
|
address: {
|
||||||
|
'@type': 'PostalAddress',
|
||||||
|
addressLocality: companyDetails.address.city,
|
||||||
|
addressRegion: companyDetails.address.region,
|
||||||
|
addressCountry: 'PL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
areaServed: { '@type': 'AdministrativeArea', name: 'Dolny Śląsk' },
|
||||||
|
description: 'Serwis, naprawa i przegląd klimatyzatorów LG na Dolnym Śląsku.',
|
||||||
|
mainEntityOfPage: {
|
||||||
|
'@type': 'FAQPage',
|
||||||
|
mainEntity: faqItems.map(f => ({
|
||||||
|
'@type': 'Question',
|
||||||
|
name: f.q,
|
||||||
|
acceptedAnswer: { '@type': 'Answer', text: f.a },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||||
|
<main>
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="bg-primary text-white py-16 md:py-24">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-white/10 rounded-full text-sm font-medium mb-6">
|
||||||
|
<Wrench className="size-4" />
|
||||||
|
Specjalistyczny serwis LG
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl md:text-5xl font-extrabold font-heading leading-tight mb-6">
|
||||||
|
Serwis klimatyzacji LG<br />
|
||||||
|
<span className="text-accent">tam gdzie inni mówią nie</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-white/80 mb-8 max-w-2xl">
|
||||||
|
Klimatyzatory LG mają opinię trudnych w serwisie — większość firm odmawia ich naprawy.
|
||||||
|
My mamy odpowiednie doświadczenie i narzędzia, żeby skutecznie diagnozować
|
||||||
|
i naprawiać te urządzenia na terenie Dolnego Śląska.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
|
<a href={`tel:${companyDetails.phoneRaw}`}>
|
||||||
|
<button className="flex items-center gap-2 px-6 py-3 bg-accent text-white rounded-xl font-bold hover:bg-accent/90 transition-colors">
|
||||||
|
<Phone className="size-5" /> Zadzwoń: {companyDetails.phone}
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<Link href="/kontakt">
|
||||||
|
<button className="flex items-center gap-2 px-6 py-3 bg-white/10 text-white rounded-xl font-bold hover:bg-white/20 transition-colors border border-white/20">
|
||||||
|
Napisz do nas <ArrowRight className="size-5" />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Dlaczego my */}
|
||||||
|
<section className="py-16 bg-white">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-start gap-4 p-5 bg-orange-50 border border-orange-200 rounded-2xl mb-10">
|
||||||
|
<AlertTriangle className="size-6 text-orange-500 shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-primary mb-1">Dlaczego inne firmy odmawiają serwisu LG?</p>
|
||||||
|
<p className="text-sm text-text-muted">
|
||||||
|
Klimatyzatory LG wymagają specjalistycznej wiedzy i dedykowanych narzędzi diagnostycznych.
|
||||||
|
Wiele firm serwisowych unika tych urządzeń ze względu na złożoność układów elektronicznych
|
||||||
|
i trudniejszy dostęp do oryginalnych części zamiennych. My podjęliśmy to wyzwanie
|
||||||
|
i dziś skutecznie naprawiamy to, od czego inni odchodzą.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-extrabold text-primary font-heading mb-8">Co robimy dla Twojego LG?</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{[
|
||||||
|
{ title: 'Przegląd i czyszczenie', desc: 'Czyszczenie filtrów, lameli, tacy ociekowej. Sprawdzenie szczelności i ciśnienia czynnika R32/R410A.' },
|
||||||
|
{ title: 'Diagnostyka usterek', desc: 'Odczyt kodów błędów, pomiary elektryczne, identyfikacja uszkodzonych podzespołów.' },
|
||||||
|
{ title: 'Naprawa układu chłodniczego', desc: 'Uzupełnianie czynnika chłodniczego, naprawa nieszczelności, wymiana sprężarki.' },
|
||||||
|
{ title: 'Naprawa elektroniki', desc: 'Wymiana płyt głównych, modułów WiFi, czujników temperatury i sterowników.' },
|
||||||
|
{ title: 'Odgrzybianie i dezynfekcja', desc: 'Profesjonalne odgrzybianie klimatyzatora — eliminacja grzybów i bakterii z układu nawiewu.' },
|
||||||
|
{ title: 'Serwis gwarancyjny i pogwarancyjny', desc: 'Obsługujemy urządzenia zarówno w trakcie jak i po wygaśnięciu gwarancji producenta.' },
|
||||||
|
].map((item, i) => (
|
||||||
|
<div key={i} className="flex gap-3 p-4 bg-slate-50 rounded-xl">
|
||||||
|
<Check className="size-5 text-accent shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-primary text-sm">{item.title}</p>
|
||||||
|
<p className="text-xs text-text-muted mt-0.5">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Obsługiwane serie LG */}
|
||||||
|
<section className="py-16 bg-slate-50">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h2 className="text-2xl font-extrabold text-primary font-heading mb-6">
|
||||||
|
Obsługiwane serie klimatyzatorów LG
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{['LG Artcool', 'LG Standard Plus', 'LG Dualcool', 'LG MultiSplit', 'LG LGRED°', 'LG Inverter V', 'LG UV Nano', 'LG Deluxe'].map(s => (
|
||||||
|
<span key={s} className="px-4 py-2 bg-white border border-border rounded-xl text-sm font-semibold text-primary">
|
||||||
|
{s}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-text-muted mt-4">
|
||||||
|
Nie widzisz swojego modelu? Zadzwoń — prawdopodobnie też go obsługujemy.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FAQ */}
|
||||||
|
<section className="py-16 bg-white">
|
||||||
|
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h2 className="text-2xl font-extrabold text-primary font-heading mb-8">
|
||||||
|
Często zadawane pytania
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{faqItems.map((item, i) => (
|
||||||
|
<details key={i} className="group bg-slate-50 rounded-xl border border-border overflow-hidden">
|
||||||
|
<summary className="flex items-center justify-between p-5 cursor-pointer font-semibold text-primary text-sm list-none">
|
||||||
|
{item.q}
|
||||||
|
<ArrowRight className="size-4 text-text-muted group-open:rotate-90 transition-transform shrink-0 ml-3" />
|
||||||
|
</summary>
|
||||||
|
<div className="px-5 pb-5 text-sm text-text-muted leading-relaxed border-t border-border pt-4">
|
||||||
|
{item.a}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<section className="py-16 bg-primary text-white">
|
||||||
|
<div className="max-w-2xl mx-auto px-4 text-center">
|
||||||
|
<h2 className="text-2xl font-extrabold font-heading mb-4">
|
||||||
|
Masz problem z klimatyzatorem LG?
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/80 mb-8">
|
||||||
|
Zadzwoń lub napisz — odpowiadamy szybko i przyjeżdżamy na Dolny Śląsk.
|
||||||
|
</p>
|
||||||
|
<a href={`tel:${companyDetails.phoneRaw}`}>
|
||||||
|
<button className="flex items-center gap-2 px-8 py-4 bg-accent text-white rounded-xl font-bold text-lg hover:bg-accent/90 transition-colors mx-auto">
|
||||||
|
<Phone className="size-5" /> {companyDetails.phone}
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
'/regulamin',
|
'/regulamin',
|
||||||
'/polityka-prywatnosci',
|
'/polityka-prywatnosci',
|
||||||
'/reklamacje',
|
'/reklamacje',
|
||||||
|
'/serwis-lg',
|
||||||
|
'/zgloszenie-awarii',
|
||||||
'/dostawa',
|
'/dostawa',
|
||||||
'/gwarancja',
|
'/gwarancja',
|
||||||
].map((route) => ({
|
].map((route) => ({
|
||||||
|
|||||||
@@ -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
@@ -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} />
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { Resend } from 'resend';
|
||||||
|
|
||||||
|
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||||
|
|
||||||
|
export async function submitAwariaForm(data: {
|
||||||
|
isUrgent: boolean;
|
||||||
|
brand: string;
|
||||||
|
model: string;
|
||||||
|
errorCode: string;
|
||||||
|
problemType: string;
|
||||||
|
description: string;
|
||||||
|
warranty: string;
|
||||||
|
lastService: string;
|
||||||
|
name: string;
|
||||||
|
phone: string;
|
||||||
|
city: string;
|
||||||
|
preferredDate: string;
|
||||||
|
preferredTime: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
await resend.emails.send({
|
||||||
|
from: process.env.RESEND_FROM_EMAIL || '[email protected]',
|
||||||
|
to: process.env.RESEND_TO_EMAIL || '[email protected]',
|
||||||
|
subject: `${data.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe: ${data.brand} — ${data.city}`,
|
||||||
|
html: `
|
||||||
|
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;">
|
||||||
|
<div style="background:${data.isUrgent ? '#ef4444' : '#0F2A47'};padding:20px;text-align:center;">
|
||||||
|
<h1 style="color:#fff;margin:0;font-size:20px;">
|
||||||
|
${data.isUrgent ? '🚨 PILNE — ' : ''}Zgłoszenie serwisowe ThermCool
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div style="padding:24px;">
|
||||||
|
<h2 style="font-size:16px;margin-top:0;">Dane klienta</h2>
|
||||||
|
<p><strong>Imię:</strong> ${data.name}</p>
|
||||||
|
<p><strong>Telefon:</strong> <a href="tel:${data.phone}">${data.phone}</a></p>
|
||||||
|
<p><strong>Miasto:</strong> ${data.city}</p>
|
||||||
|
${data.preferredDate ? `<p><strong>Preferowany termin:</strong> ${data.preferredDate} ${data.preferredTime || ''}</p>` : ''}
|
||||||
|
|
||||||
|
<h2 style="font-size:16px;">Urządzenie</h2>
|
||||||
|
<p><strong>Marka:</strong> ${data.brand}</p>
|
||||||
|
${data.model ? `<p><strong>Model:</strong> ${data.model}</p>` : ''}
|
||||||
|
${data.warranty ? `<p><strong>Gwarancja:</strong> ${data.warranty}</p>` : ''}
|
||||||
|
${data.lastService ? `<p><strong>Ostatni serwis:</strong> ${data.lastService}</p>` : ''}
|
||||||
|
|
||||||
|
<h2 style="font-size:16px;">Problem</h2>
|
||||||
|
<p><strong>Typ problemu:</strong> ${data.problemType}</p>
|
||||||
|
${data.errorCode ? `<p><strong>Kod błędu:</strong> ${data.errorCode}</p>` : ''}
|
||||||
|
${data.description ? `<p><strong>Opis:</strong> ${data.description}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
} catch {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { AwariaClient } from '@/components/awaria/AwariaClient';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Zgłoś awarię klimatyzacji | Serwis ThermCool Wrocław',
|
||||||
|
description: 'Zgłoś awarię klimatyzatora online. Szybka diagnostyka, kod błędu, preferowany termin wizyty. Obsługujemy Wrocław i cały Dolny Śląsk.',
|
||||||
|
alternates: { canonical: '/zgloszenie-awarii' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ZgloszenieAwariiPage() {
|
||||||
|
return <AwariaClient />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { AlertTriangle, Wrench, Phone, Check, Clock, Zap } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { companyDetails } from '@/content/company';
|
||||||
|
import { submitAwariaForm } from '@/app/zgloszenie-awarii/actions';
|
||||||
|
|
||||||
|
// Kody błędów popularnych marek
|
||||||
|
const ERROR_CODES: Record<string, Record<string, string>> = {
|
||||||
|
'Gree': {
|
||||||
|
'E1': 'Błąd czujnika temperatury powietrza wewnętrznego',
|
||||||
|
'E2': 'Błąd czujnika temperatury wymiennika wewnętrznego',
|
||||||
|
'E3': 'Błąd czujnika temperatury zewnętrznej',
|
||||||
|
'E4': 'Wysokie ciśnienie — możliwy problem ze sprężarką lub skraplaczem',
|
||||||
|
'E5': 'Niskie ciśnienie — możliwy brak czynnika chłodniczego',
|
||||||
|
'E6': 'Błąd komunikacji między jednostkami',
|
||||||
|
'F1': 'Awaria czujnika temperatury — wymagana wymiana czujnika',
|
||||||
|
'H6': 'Blokada silnika wentylatora wewnętrznego',
|
||||||
|
},
|
||||||
|
'LG': {
|
||||||
|
'CH01': 'Błąd czujnika temperatury powietrza wewnętrznego',
|
||||||
|
'CH02': 'Błąd czujnika wymiennika wewnętrznego',
|
||||||
|
'CH03': 'Błąd czujnika wymiennika zewnętrznego',
|
||||||
|
'CH05': 'Błąd komunikacji — sprawdź okablowanie',
|
||||||
|
'CH07': 'Awaria wentylatora zewnętrznego',
|
||||||
|
'CH10': 'Błąd czujnika prądu sprężarki',
|
||||||
|
'CH38': 'Błąd modułu WiFi',
|
||||||
|
'C1': 'Problem z drenażem — sprawdź odpływ skroplin',
|
||||||
|
},
|
||||||
|
'Daikin': {
|
||||||
|
'A1': 'Awaria PCB — płyta główna jednostki wewnętrznej',
|
||||||
|
'A5': 'Przegrzanie — wymagane czyszczenie lub sprawdzenie czynnika',
|
||||||
|
'C4': 'Błąd czujnika wymiennika wewnętrznego',
|
||||||
|
'E1': 'Awaria PCB jednostki zewnętrznej',
|
||||||
|
'E3': 'Wyłącznik wysokiego ciśnienia',
|
||||||
|
'E4': 'Wyłącznik niskiego ciśnienia',
|
||||||
|
'F3': 'Niskie ciśnienie oleju sprężarki',
|
||||||
|
'U4': 'Błąd komunikacji wewnętrzna-zewnętrzna',
|
||||||
|
},
|
||||||
|
'Samsung': {
|
||||||
|
'E1': 'Błąd czujnika temperatury powietrza',
|
||||||
|
'E2': 'Błąd czujnika wymiennika ciepła',
|
||||||
|
'E3': 'Błąd silnika wentylatora',
|
||||||
|
'E4': 'Wyłącznik wysokiego ciśnienia',
|
||||||
|
'E6': 'Błąd komunikacji',
|
||||||
|
'F1': 'Awaria czujnika — wymagana diagnostyka',
|
||||||
|
'C4': 'Błąd czujnika temperatury zewnętrznej',
|
||||||
|
},
|
||||||
|
'Haier': {
|
||||||
|
'E1': 'Błąd czujnika temperatury wewnętrznej',
|
||||||
|
'E2': 'Błąd czujnika wymiennika wewnętrznego',
|
||||||
|
'E3': 'Błąd czujnika temperatury zewnętrznej',
|
||||||
|
'E4': 'Wyłącznik wysokiego ciśnienia',
|
||||||
|
'E5': 'Wyłącznik niskiego ciśnienia — sprawdź poziom czynnika',
|
||||||
|
'E6': 'Błąd komunikacji',
|
||||||
|
'F1': 'Awaria czujnika — wymiana',
|
||||||
|
},
|
||||||
|
'Rotenso': {
|
||||||
|
'E1': 'Błąd czujnika temperatury wewnętrznej',
|
||||||
|
'E2': 'Błąd wymiennika wewnętrznego',
|
||||||
|
'E3': 'Błąd czujnika zewnętrznego',
|
||||||
|
'E5': 'Niskie ciśnienie — sprawdź czynnik chłodniczy',
|
||||||
|
'E6': 'Błąd komunikacji między jednostkami',
|
||||||
|
'P1': 'Ochrona przed zamrożeniem',
|
||||||
|
'P4': 'Wyłącznik wysokiego ciśnienia',
|
||||||
|
},
|
||||||
|
'Mitsubishi Electric': {
|
||||||
|
'E1': 'Błąd czujnika wewnętrznego',
|
||||||
|
'E2': 'Błąd czujnika wymiennika',
|
||||||
|
'E4': 'Błąd czujnika zewnętrznego',
|
||||||
|
'E6': 'Problem z wentylatorem zewnętrznym',
|
||||||
|
'P1': 'Błąd czujnika powietrza zewnętrznego',
|
||||||
|
'P4': 'Wyłącznik wysokiego ciśnienia',
|
||||||
|
'P8': 'Przegrzanie sprężarki',
|
||||||
|
'U1': 'Błąd napięcia zasilania',
|
||||||
|
},
|
||||||
|
'Toshiba': {
|
||||||
|
'E01': 'Błąd czujnika temperatury wewnętrznej',
|
||||||
|
'E02': 'Błąd czujnika wymiennika wewnętrznego',
|
||||||
|
'E03': 'Błąd czujnika zewnętrznego',
|
||||||
|
'E04': 'Wyłącznik wysokiego ciśnienia',
|
||||||
|
'E07': 'Błąd komunikacji',
|
||||||
|
'E08': 'Awaria wentylatora zewnętrznego',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const BRANDS = ['Gree', 'Rotenso', 'Haier', 'LG', 'Samsung', 'Daikin', 'Mitsubishi Electric', 'Toshiba', 'Aux', 'Fuji Electric', 'Inna marka'];
|
||||||
|
const PROBLEM_TYPES = [
|
||||||
|
'Nie chłodzi / nie grzeje',
|
||||||
|
'Wyświetla kod błędu',
|
||||||
|
'Głośna praca / wibracje',
|
||||||
|
'Wyciek wody',
|
||||||
|
'Brzydki zapach',
|
||||||
|
'Nie włącza się',
|
||||||
|
'Pilot nie działa',
|
||||||
|
'Zamarza wymiennik',
|
||||||
|
'Inne',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function AwariaClient() {
|
||||||
|
const [step, setStep] = useState<'form' | 'success'>('form');
|
||||||
|
const [isUrgent, setIsUrgent] = useState(false);
|
||||||
|
const [brand, setBrand] = useState('');
|
||||||
|
const [model, setModel] = useState('');
|
||||||
|
const [errorCode, setErrorCode] = useState('');
|
||||||
|
const [problemType, setProblemType] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [lastService, setLastService] = useState('');
|
||||||
|
const [warranty, setWarranty] = useState('');
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [phone, setPhone] = useState('');
|
||||||
|
const [city, setCity] = useState('');
|
||||||
|
const [preferredDate, setPreferredDate] = useState('');
|
||||||
|
const [preferredTime, setPreferredTime] = useState('');
|
||||||
|
const [consent, setConsent] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Podpowiedź kodu błędu
|
||||||
|
const errorHint = brand && errorCode && ERROR_CODES[brand]?.[errorCode.toUpperCase()];
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!consent) {
|
||||||
|
alert('Proszę zaznaczyć zgodę na przetwarzanie danych.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
await submitAwariaForm({
|
||||||
|
isUrgent,
|
||||||
|
brand,
|
||||||
|
model,
|
||||||
|
errorCode,
|
||||||
|
problemType,
|
||||||
|
description,
|
||||||
|
warranty,
|
||||||
|
lastService,
|
||||||
|
name,
|
||||||
|
phone,
|
||||||
|
city,
|
||||||
|
preferredDate,
|
||||||
|
preferredTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setStep('success');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (step === 'success') {
|
||||||
|
return (
|
||||||
|
<main className="bg-slate-50 min-h-screen flex items-center justify-center py-16">
|
||||||
|
<div className="max-w-md mx-auto px-4 text-center">
|
||||||
|
<div className="size-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<Check className="size-10 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-extrabold text-primary font-heading mb-3">
|
||||||
|
Zgłoszenie przyjęte!
|
||||||
|
</h1>
|
||||||
|
<p className="text-text-muted mb-2">
|
||||||
|
{isUrgent
|
||||||
|
? 'Twoje zgłoszenie oznaczono jako PILNE — skontaktujemy się z Tobą w ciągu 2 godzin.'
|
||||||
|
: 'Skontaktujemy się z Tobą w ciągu 24 godzin, aby ustalić termin wizyty serwisanta.'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-text-muted mb-8">
|
||||||
|
Jeśli sprawa jest pilna — zadzwoń bezpośrednio:
|
||||||
|
</p>
|
||||||
|
<a href={`tel:${companyDetails.phoneRaw}`}>
|
||||||
|
<button className="flex items-center gap-2 px-6 py-3 bg-accent text-white rounded-xl font-bold mx-auto hover:bg-accent/90 transition-colors mb-4">
|
||||||
|
<Phone className="size-4" /> {companyDetails.phone}
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<Link href="/">
|
||||||
|
<button className="text-sm text-text-muted hover:text-accent transition-colors">
|
||||||
|
Wróć na stronę główną
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="bg-slate-50 min-h-screen py-12">
|
||||||
|
<div className="max-w-2xl mx-auto px-4 sm:px-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className="size-14 bg-red-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Wrench className="size-7 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-extrabold text-primary font-heading mb-2">
|
||||||
|
Zgłoś awarię / serwis
|
||||||
|
</h1>
|
||||||
|
<p className="text-text-muted">Wypełnij formularz — oddzwonimy i ustalimy termin wizyty</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pilne zgłoszenie toggle */}
|
||||||
|
<div
|
||||||
|
onClick={() => setIsUrgent(!isUrgent)}
|
||||||
|
className={`flex items-center gap-4 p-4 rounded-2xl border-2 cursor-pointer mb-6 transition-all ${
|
||||||
|
isUrgent ? 'border-red-400 bg-red-50' : 'border-border bg-white hover:border-red-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`size-10 rounded-xl flex items-center justify-center shrink-0 ${isUrgent ? 'bg-red-500' : 'bg-slate-100'}`}>
|
||||||
|
<AlertTriangle className={`size-5 ${isUrgent ? 'text-white' : 'text-text-muted'}`} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className={`font-bold text-sm ${isUrgent ? 'text-red-700' : 'text-primary'}`}>
|
||||||
|
{isUrgent ? '🚨 ZGŁOSZENIE PILNE — kontakt w ciągu 2 godzin' : 'Oznacz jako PILNE'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-text-muted">Np. upał, brak chłodzenia, awaria krytyczna</p>
|
||||||
|
</div>
|
||||||
|
<div className={`size-6 rounded-full border-2 flex items-center justify-center shrink-0 ${isUrgent ? 'border-red-500 bg-red-500' : 'border-slate-300'}`}>
|
||||||
|
{isUrgent && <Check className="size-3.5 text-white" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
|
||||||
|
|
||||||
|
{/* Sekcja 1: Urządzenie */}
|
||||||
|
<div className="bg-white rounded-2xl border border-border p-6">
|
||||||
|
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
|
||||||
|
<Zap className="size-4 text-accent" /> Urządzenie
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">Marka *</label>
|
||||||
|
<select
|
||||||
|
required value={brand} onChange={e => { setBrand(e.target.value); setErrorCode(''); }}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
|
||||||
|
>
|
||||||
|
<option value="">Wybierz markę...</option>
|
||||||
|
{BRANDS.map(b => <option key={b} value={b}>{b}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">Model (opcjonalnie)</label>
|
||||||
|
<input
|
||||||
|
value={model} onChange={e => setModel(e.target.value)}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
|
||||||
|
placeholder="Np. Gree Pular, LG Artcool 3,5 kW..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">Na gwarancji?</label>
|
||||||
|
<select
|
||||||
|
value={warranty} onChange={e => setWarranty(e.target.value)}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
|
||||||
|
>
|
||||||
|
<option value="">Nie wiem</option>
|
||||||
|
<option value="tak">Tak</option>
|
||||||
|
<option value="nie">Nie</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">Ostatni serwis</label>
|
||||||
|
<select
|
||||||
|
value={lastService} onChange={e => setLastService(e.target.value)}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
|
||||||
|
>
|
||||||
|
<option value="">Nie pamiętam</option>
|
||||||
|
<option value="<1rok">Mniej niż rok temu</option>
|
||||||
|
<option value="1-2lata">1-2 lata temu</option>
|
||||||
|
<option value="2-5lat">2-5 lat temu</option>
|
||||||
|
<option value=">5lat">Ponad 5 lat temu</option>
|
||||||
|
<option value="nigdy">Nigdy nie był serwisowany</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sekcja 2: Problem */}
|
||||||
|
<div className="bg-white rounded-2xl border border-border p-6">
|
||||||
|
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="size-4 text-accent" /> Opis problemu
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-2 block">Rodzaj problemu *</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{PROBLEM_TYPES.map(p => (
|
||||||
|
<button
|
||||||
|
key={p} type="button"
|
||||||
|
onClick={() => setProblemType(p)}
|
||||||
|
className={`px-3 py-1.5 rounded-xl text-xs font-semibold border-2 transition-all ${
|
||||||
|
problemType === p ? 'border-accent bg-accent text-white' : 'border-border bg-white text-primary hover:border-accent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kod błędu z podpowiedzią */}
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">
|
||||||
|
Kod błędu z wyświetlacza (opcjonalnie)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
value={errorCode}
|
||||||
|
onChange={e => setErrorCode(e.target.value)}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
|
||||||
|
placeholder="Np. E1, F3, CH05..."
|
||||||
|
/>
|
||||||
|
{errorHint && (
|
||||||
|
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-xl text-xs text-blue-700">
|
||||||
|
<span className="font-bold">Kod {errorCode.toUpperCase()}:</span> {errorHint}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{errorCode && !errorHint && brand && ERROR_CODES[brand] && (
|
||||||
|
<p className="mt-1 text-xs text-text-muted">Kodu nie rozpoznano — serwisant zdiagnozuje na miejscu.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">Szczegółowy opis</label>
|
||||||
|
<textarea
|
||||||
|
value={description} onChange={e => setDescription(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none resize-none"
|
||||||
|
placeholder="Opisz kiedy się pojawił problem, jak często występuje, czy jest jakiś charakterystyczny dźwięk..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sekcja 3: Termin */}
|
||||||
|
<div className="bg-white rounded-2xl border border-border p-6">
|
||||||
|
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
|
||||||
|
<Clock className="size-4 text-accent" /> Preferowany termin
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">Data</label>
|
||||||
|
<input
|
||||||
|
type="date" value={preferredDate} onChange={e => setPreferredDate(e.target.value)}
|
||||||
|
min={new Date().toISOString().split('T')[0]}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">Godzina</label>
|
||||||
|
<select
|
||||||
|
value={preferredTime} onChange={e => setPreferredTime(e.target.value)}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
|
||||||
|
>
|
||||||
|
<option value="">Dowolna</option>
|
||||||
|
<option value="8:00-10:00">8:00 – 10:00</option>
|
||||||
|
<option value="10:00-12:00">10:00 – 12:00</option>
|
||||||
|
<option value="12:00-14:00">12:00 – 14:00</option>
|
||||||
|
<option value="14:00-16:00">14:00 – 16:00</option>
|
||||||
|
<option value="16:00-18:00">16:00 – 18:00</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sekcja 4: Dane kontaktowe */}
|
||||||
|
<div className="bg-white rounded-2xl border border-border p-6">
|
||||||
|
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
|
||||||
|
<Phone className="size-4 text-accent" /> Dane kontaktowe
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">Imię i nazwisko *</label>
|
||||||
|
<input
|
||||||
|
required value={name} onChange={e => setName(e.target.value)}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
|
||||||
|
placeholder="Jan Kowalski"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">Telefon *</label>
|
||||||
|
<input
|
||||||
|
required type="tel" value={phone} onChange={e => setPhone(e.target.value)}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none"
|
||||||
|
placeholder="+48 600 000 000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-primary mb-1 block">Miasto *</label>
|
||||||
|
<select
|
||||||
|
required value={city} onChange={e => setCity(e.target.value)}
|
||||||
|
className="w-full border border-border rounded-xl px-3 py-2.5 text-sm focus:border-accent outline-none bg-white"
|
||||||
|
>
|
||||||
|
<option value="">Wybierz miasto...</option>
|
||||||
|
{['Wrocław', 'Legnica', 'Wałbrzych', 'Jelenia Góra', 'Lubin', 'Głogów', 'Świdnica', 'Dzierżoniów', 'Oleśnica', 'Oława', 'Trzebnica', 'Środa Śląska', 'Siechnice', 'Inne'].map(c => (
|
||||||
|
<option key={c} value={c}>{c}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Zgoda RODO */}
|
||||||
|
<div
|
||||||
|
className="flex items-start gap-3 cursor-pointer"
|
||||||
|
onClick={() => setConsent(!consent)}
|
||||||
|
>
|
||||||
|
<div className={`size-5 rounded border-2 shrink-0 mt-0.5 flex items-center justify-center transition-colors ${
|
||||||
|
consent ? 'bg-accent border-accent' : 'bg-white border-slate-300'
|
||||||
|
}`}>
|
||||||
|
{consent && <Check className="size-3 text-white" />}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-text-muted leading-relaxed">
|
||||||
|
Wyrażam zgodę na przetwarzanie danych osobowych podanych w formularzu
|
||||||
|
w celu obsługi zgłoszenia serwisowego przez firmę ThermCool. *
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !brand || !problemType || !name || !phone || !city || !consent}
|
||||||
|
className={`flex items-center justify-center gap-2 py-4 px-6 rounded-2xl font-bold text-base transition-all ${
|
||||||
|
isUrgent
|
||||||
|
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||||
|
: 'bg-accent hover:bg-accent/90 text-white'
|
||||||
|
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Wysyłanie...' : isUrgent ? '🚨 Wyślij PILNE zgłoszenie' : 'Wyślij zgłoszenie serwisowe'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p className="text-xs text-text-muted text-center">
|
||||||
|
{isUrgent ? 'Odpowiadamy na pilne zgłoszenia w ciągu 2 godzin w godzinach pracy.' : 'Skontaktujemy się w ciągu 24 godzin.'}
|
||||||
|
{' '}Możesz też zadzwonić bezpośrednio: <a href={`tel:${companyDetails.phoneRaw}`} className="text-accent font-semibold">{companyDetails.phone}</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,27 +4,27 @@ import React, { useState } from 'react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { 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,26 +139,53 @@ 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>
|
||||||
|
|
||||||
{/* Temat */}
|
{/* Miasto */}
|
||||||
<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="city" className="text-sm font-semibold text-primary">Miasto <span className="text-red-500">*</span></label>
|
||||||
<select
|
<select
|
||||||
{...register("subject")}
|
{...register("city")}
|
||||||
id="subject"
|
id="city"
|
||||||
className={`w-full px-4 py-3 rounded-xl border ${errors.subject ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 appearance-none`}
|
required
|
||||||
|
className={`w-full px-4 py-3 rounded-xl border ${errors.city ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 appearance-none`}
|
||||||
>
|
>
|
||||||
<option value="">Wybierz temat...</option>
|
<option value="">Wybierz miasto...</option>
|
||||||
<option value="klimatyzacja">Klimatyzacja</option>
|
<option value="Wrocław">Wrocław</option>
|
||||||
<option value="pompy-ciepla">Pompy ciepła</option>
|
<option value="Legnica">Legnica</option>
|
||||||
<option value="remonty">Remonty i wykończenia</option>
|
<option value="Wałbrzych">Wałbrzych</option>
|
||||||
<option value="hydraulika">Hydraulika</option>
|
<option value="Jelenia Góra">Jelenia Góra</option>
|
||||||
<option value="serwis-agd">Serwis AGD</option>
|
<option value="Lubin">Lubin</option>
|
||||||
<option value="inne">Inne zapytanie</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>
|
</select>
|
||||||
{errors.subject && <p className="text-red-500 text-xs font-medium">{errors.subject.message}</p>}
|
{errors.city && <p className="text-red-500 text-xs font-medium">{errors.city.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Temat */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="subject" className="text-sm font-semibold text-primary">Temat rozmowy</label>
|
||||||
|
<select
|
||||||
|
{...register("subject")}
|
||||||
|
id="subject"
|
||||||
|
className={`w-full px-4 py-3 rounded-xl border ${errors.subject ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 appearance-none`}
|
||||||
|
>
|
||||||
|
<option value="">Wybierz temat...</option>
|
||||||
|
<option value="klimatyzacja">Klimatyzacja</option>
|
||||||
|
<option value="pompy-ciepla">Pompy ciepła</option>
|
||||||
|
<option value="remonty">Remonty i wykończenia</option>
|
||||||
|
<option value="hydraulika">Hydraulika</option>
|
||||||
|
<option value="serwis-agd">Serwis AGD</option>
|
||||||
|
<option value="inne">Inne zapytanie</option>
|
||||||
|
</select>
|
||||||
|
{errors.subject && <p className="text-red-500 text-xs font-medium">{errors.subject.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Wiadomość */}
|
{/* Wiadomość */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="message" className="text-sm font-semibold text-primary">Wiadomość</label>
|
<label htmlFor="message" className="text-sm font-semibold text-primary">Wiadomość</label>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (p.price === null || p.price > maxPrice) return false;
|
|
||||||
|
if (preferredBrand && p.brand !== preferredBrand) return false;
|
||||||
|
|
||||||
|
if (category === 'multisplit') {
|
||||||
|
if (p.price !== null && p.price > maxPrice) return false;
|
||||||
|
} else {
|
||||||
|
if (p.price === null || p.price > maxPrice) return false;
|
||||||
|
}
|
||||||
|
|
||||||
const moc = parseFloat(String(p.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]);
|
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);
|
||||||
})
|
});
|
||||||
.slice(0, 6);
|
|
||||||
}, [showResults, products, requiredPower, category, budget, installCost.min]);
|
return filtered.length > 0
|
||||||
|
? filtered.slice(0, 6)
|
||||||
|
: products
|
||||||
|
.filter(p => p.category === category && p.price !== null)
|
||||||
|
.sort((a, b) => (a.price || 0) - (b.price || 0))
|
||||||
|
.slice(0, 6);
|
||||||
|
}, [showResults, deviceType, products, requiredPower, category, budget, preferredBrand]);
|
||||||
|
|
||||||
const handleFinish = () => setShowResults(true);
|
const 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,55 +166,69 @@ 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 => {
|
||||||
<div key={product.slug} className="bg-white rounded-2xl border border-border overflow-hidden hover:border-accent hover:shadow-md transition-all">
|
const mocChlodnicza = parseFloat(
|
||||||
<div className="relative aspect-[4/3] bg-slate-50">
|
String(product.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]
|
||||||
<Image
|
);
|
||||||
src={product.image}
|
const seer = mocChlodnicza > 0 ? mocChlodnicza / (mocChlodnicza * 0.5) : 5;
|
||||||
alt={product.name}
|
const pobor = mocChlodnicza / 5; // szacowany pobór przy SEER~5
|
||||||
fill
|
const zuzycieMiesiac = (pobor * 8 * 30).toFixed(0); // 8h/dzień 30 dni
|
||||||
className="object-contain p-6"
|
|
||||||
unoptimized={product.image.startsWith('http')}
|
return (
|
||||||
/>
|
<div key={product.slug} className="bg-white rounded-2xl border border-border overflow-hidden hover:border-accent hover:shadow-md transition-all">
|
||||||
{product.specs?.['Klasa energetyczna'] && (
|
<div className="relative aspect-[4/3] bg-slate-50">
|
||||||
<span className="absolute top-2 left-2 px-2 py-0.5 bg-green-500 text-white text-xs font-bold rounded">
|
<Image
|
||||||
{product.specs['Klasa energetyczna'].split('/')[0]}
|
src={product.image}
|
||||||
</span>
|
alt={product.name}
|
||||||
)}
|
fill
|
||||||
</div>
|
className="object-contain p-6"
|
||||||
<div className="p-4">
|
unoptimized={product.image.startsWith('http')}
|
||||||
<p className="text-xs text-accent font-bold uppercase mb-1">{product.brand}</p>
|
/>
|
||||||
<h3 className="text-sm font-bold text-primary font-heading leading-snug mb-3 line-clamp-2">
|
{product.specs?.['Klasa energetyczna'] && (
|
||||||
{product.name}
|
<span className="absolute top-2 left-2 px-2 py-0.5 bg-green-500 text-white text-xs font-bold rounded">
|
||||||
</h3>
|
{product.specs['Klasa energetyczna'].split('/')[0]}
|
||||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
|
||||||
{product.specs?.['Moc chłodnicza'] && (
|
|
||||||
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded border border-blue-100">
|
|
||||||
{product.specs['Moc chłodnicza']} kW
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{product.specs?.['Hałas wewnątrz (dB)'] && (
|
|
||||||
<span className="px-2 py-0.5 bg-slate-50 text-text-muted text-xs rounded border border-border">
|
|
||||||
{product.specs['Hałas wewnątrz (dB)']} dB
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end justify-between">
|
<div className="p-4 flex flex-col h-full">
|
||||||
<div>
|
<p className="text-xs text-accent font-bold uppercase mb-1">{product.brand}</p>
|
||||||
<p className="text-lg font-extrabold text-primary font-heading leading-none">
|
<h3 className="text-sm font-bold text-primary font-heading leading-snug mb-3 line-clamp-2">
|
||||||
{product.price?.toLocaleString('pl-PL')} zł
|
{product.name}
|
||||||
</p>
|
</h3>
|
||||||
<p className="text-xs text-text-muted mt-0.5">+ montaż od {installCost.min.toLocaleString('pl-PL')} zł</p>
|
<div className="flex flex-wrap gap-1.5 mb-1">
|
||||||
|
{product.specs?.['Moc chłodnicza'] && (
|
||||||
|
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded border border-blue-100">
|
||||||
|
{product.specs['Moc chłodnicza']} kW
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{product.specs?.['Hałas wewnątrz (dB)'] && (
|
||||||
|
<span className="px-2 py-0.5 bg-slate-50 text-text-muted text-xs rounded border border-border">
|
||||||
|
{product.specs['Hałas wewnątrz (dB)']} dB
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-text-muted mt-1 mb-4">
|
||||||
|
~{zuzycieMiesiac} kWh/miesiąc (8h/dzień)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-end justify-between mt-auto">
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-extrabold text-primary font-heading leading-none">
|
||||||
|
{product.price?.toLocaleString('pl-PL')} zł
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-text-muted mt-0.5">+ montaż od {installCost.min.toLocaleString('pl-PL')} zł</p>
|
||||||
|
</div>
|
||||||
|
<Link href={`/sklep/${product.slug}`}>
|
||||||
|
<button className="flex items-center gap-1.5 px-3 py-2 bg-accent text-white rounded-xl text-xs font-bold hover:bg-accent/90 transition-colors">
|
||||||
|
Zobacz <ArrowRight className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Link href={`/sklep/${product.slug}`}>
|
|
||||||
<button className="flex items-center gap-1.5 px-3 py-2 bg-accent text-white rounded-xl text-xs font-bold hover:bg-accent/90 transition-colors">
|
|
||||||
Zobacz <ArrowRight className="size-3.5" />
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</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,45 +497,64 @@ 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 className="grid grid-cols-2 gap-3">
|
<div>
|
||||||
{[
|
<label className="text-sm font-semibold text-primary mb-3 block">Wybierz typ systemu</label>
|
||||||
{ value: 1, label: '1 pomieszczenie', sub: 'Klimatyzator split', icon: '🏠' },
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
{ value: 2, label: '2 pomieszczenia', sub: 'System multisplit 2+1', icon: '🏡' },
|
{[
|
||||||
{ value: 3, label: '3 pomieszczenia', sub: 'System multisplit 3+1', icon: '🏘️' },
|
{ value: 'auto', label: 'Dobierz automatycznie' },
|
||||||
{ value: 4, label: '4-5 pomieszczeń', sub: 'System multisplit 4+1', icon: '🏗️' },
|
{ value: 'scienne', label: 'Split (1 agregat = 1 pokój)' },
|
||||||
].map(o => (
|
{ value: 'multisplit', label: 'Multisplit (1 agregat = wiele)' },
|
||||||
<button key={o.value} onClick={() => setRooms(o.value)}
|
].map(o => (
|
||||||
className={`p-4 rounded-xl border-2 text-left transition-all ${rooms === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
|
<button key={o.value} onClick={() => setSystemType(o.value as 'auto'|'scienne'|'multisplit')}
|
||||||
<span className="text-2xl mb-2 block">{o.icon}</span>
|
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'}`}>
|
||||||
<p className="text-sm font-bold text-primary">{o.label}</p>
|
{o.label}
|
||||||
<p className="text-xs text-text-muted">{o.sub}</p>
|
</button>
|
||||||
</button>
|
))}
|
||||||
))}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-semibold text-primary mb-3 block">Ile pomieszczeń chcesz chłodzić?</label>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{[
|
||||||
|
{ value: 1, label: '1 pomieszczenie', sub: 'Pojedynczy pokój', icon: '🏠' },
|
||||||
|
{ value: 2, label: '2 pomieszczenia', sub: 'Dwa pokoje', icon: '🏡' },
|
||||||
|
{ value: 3, label: '3 pomieszczenia', sub: 'Trzy pokoje', icon: '🏘️' },
|
||||||
|
{ value: 4, label: '4-5 pomieszczeń', sub: 'Wiele pokoi', icon: '🏗️' },
|
||||||
|
].map(o => (
|
||||||
|
<button key={o.value} onClick={() => setRooms(o.value)}
|
||||||
|
className={`p-4 rounded-xl border-2 text-left transition-all ${rooms === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
|
||||||
|
<span className="text-2xl mb-2 block">{o.icon}</span>
|
||||||
|
<p className="text-sm font-bold text-primary">{o.label}</p>
|
||||||
|
<p className="text-xs text-text-muted">{o.sub}</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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') && (
|
||||||
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">
|
<button onClick={handleNext}
|
||||||
Dalej <ArrowRight className="size-4" />
|
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">
|
||||||
</button>
|
Dalej <ArrowRight className="size-4" />
|
||||||
|
</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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,11 +203,19 @@ 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">
|
||||||
<button className="btn btn-primary font-bold">
|
<Link href="/zgloszenie-awarii">
|
||||||
Darmowa wycena
|
<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">
|
||||||
</button>
|
<Wrench className="size-4" />
|
||||||
</Link>
|
Zgłoś awarię
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/kontakt">
|
||||||
|
<button className="btn btn-primary font-bold">
|
||||||
|
Darmowa wycena
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,55 +1,76 @@
|
|||||||
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>
|
||||||
“{test.text}”
|
<p className="text-xs text-text-muted">{t.city}</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
{/* Reviewer Details */}
|
</div>
|
||||||
<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>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
<button
|
{cartState === 'idle' && (
|
||||||
onClick={handleAddToCart}
|
<button
|
||||||
className={`flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-xl font-bold text-sm transition-all ${
|
onClick={handleAddToCart}
|
||||||
added ? 'bg-green-500 text-white' : 'bg-accent text-white hover:bg-accent/90'
|
className="flex-1 flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl font-bold text-sm bg-accent text-white hover:bg-accent/90 transition-all"
|
||||||
}`}
|
>
|
||||||
>
|
<ShoppingCart className="size-4" /> Do koszyka
|
||||||
{added ? <><Check className="size-4" /> Dodano 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>
|
||||||
|
|||||||
Generated
+39
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user