Zastosowanie wszystkich poprawek UI i logiki kalkulatora

This commit is contained in:
AI Assistant
2026-08-10 23:26:52 +02:00
commit 7324a6f800
218 changed files with 33628 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Realizacja zamówień',
description: 'Informacje o realizacji zamówień na klimatyzatory i pompy ciepła. Montaż na terenie Dolnego Śląska, wycena indywidualna.',
alternates: {
canonical: '/dostawa',
}
};
export default function DostawaPage() {
return (
<>
<h1 className="text-4xl font-extrabold mb-8">Dostawa i Płatności</h1>
<p className="lead">
Sprawdź dostępne metody wysyłki urządzeń oraz formy płatności akceptowane w naszym sklepie.
</p>
{/* TODO: treść prawna do uzupełnienia przez właściciela */}
<h2 id="dostawa">1. Metody i koszty dostawy</h2>
<p>Dla wygody naszych klientów przygotowaliśmy kilka opcji odbioru towaru:</p>
<div className="not-prose overflow-x-auto my-8 border border-border rounded-xl">
<table className="w-full text-left text-sm">
<thead className="bg-slate-50 border-b border-border text-primary font-semibold">
<tr>
<th className="p-4">Opcja dostawy</th>
<th className="p-4">Czas realizacji</th>
<th className="p-4">Koszt brutto</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
<tr className="hover:bg-slate-50 transition-colors">
<td className="p-4 font-medium">Kurier Paletowy (Klimatyzatory)</td>
<td className="p-4 text-text-muted">2-3 dni robocze</td>
<td className="p-4 font-bold text-primary">0 zł (Darmowa)</td>
</tr>
<tr className="hover:bg-slate-50 transition-colors">
<td className="p-4 font-medium">Kurier Standard (Mniejsze elementy)</td>
<td className="p-4 text-text-muted">1-2 dni robocze</td>
<td className="p-4 font-bold text-primary">19 zł</td>
</tr>
<tr className="hover:bg-slate-50 transition-colors">
<td className="p-4 font-medium">Dostawa przez naszych Instalatorów</td>
<td className="p-4 text-text-muted">W dniu montażu</td>
<td className="p-4 font-bold text-primary">0 zł</td>
</tr>
</tbody>
</table>
</div>
<h2 id="instalatorzy">2. Dostawa podczas montażu</h2>
<p>
Jeśli wybierzesz wariant zakupu &quot;Z montażem&quot;, urządzenie nie jest wysyłane z wyprzedzeniem za pośrednictwem zewnętrznej firmy kurierskiej. Sprzęt w dniu umówionej instalacji przywozi nasza własna ekipa monterska, dzięki czemu minimalizujemy ryzyko uszkodzenia urządzenia w transporcie i zdejmujemy z Ciebie obowiązek składowania gabarytowych kartonów.
</p>
<h2 id="platnosci">3. Metody płatności</h2>
<ul>
<li><strong>Przelew tradycyjny</strong> – po złożeniu zamówienia otrzymasz fakturę pro forma do opłacenia.</li>
<li><strong>Szybkie płatności (Przelewy24/PayU)</strong> – natychmiastowe księgowanie wpłaty.</li>
<li><strong>Płatność u instalatora</strong> – gotówką po zakończonym montażu i uruchomieniu sprzętu (opcja dostępna po wpłaceniu zaliczki 20%).</li>
</ul>
</>
);
}
+60
View File
@@ -0,0 +1,60 @@
import { Metadata } from 'next';
import Link from 'next/link';
import { ShieldCheck } from 'lucide-react';
export const metadata: Metadata = {
title: 'Gwarancja na Klimatyzatory i Montaż',
description: 'Szczegółowe informacje dotyczące warunków gwarancji na instalowane urządzenia oraz usługi montażowe.',
alternates: {
canonical: '/gwarancja',
}
};
export default function GwarancjaPage() {
return (
<>
<div className="flex items-center gap-4 mb-6 text-green-600">
<ShieldCheck className="w-12 h-12" />
<h1 className="text-4xl font-extrabold m-0 text-primary">Gwarancja i Serwis</h1>
</div>
<p className="lead">
Jesteśmy autoryzowanym partnerem wszystkich sprzedawanych przez nas urządzeń, co oznacza pełne bezpieczeństwo Twojej inwestycji i bezproblemową realizację świadczeń gwarancyjnych.
</p>
{/* TODO: treść prawna do uzupełnienia przez właściciela */}
<h2 id="gwarancja-urzadzenia">1. Gwarancja na urządzenia</h2>
<p>Czas trwania gwarancji zależy od wybranego producenta:</p>
<ul>
<li><strong>Daikin</strong> – 3 lata gwarancji</li>
<li><strong>Rotenso</strong> – 5 lat gwarancji</li>
<li><strong>Mitsubishi Electric</strong> – 5 lat gwarancji</li>
<li><strong>Gree</strong> – 5 lat gwarancji</li>
<li><strong>LG</strong> – do 10 lat gwarancji na sprężarkę, 3 lata na urządzenie</li>
</ul>
<div className="bg-primary-light/10 border-l-4 border-primary p-4 my-8 rounded-r-lg">
<strong>Ważne:</strong> Warunkiem zachowania gwarancji jest wykonywanie regularnych (płatnych) przeglądów technicznych zgodnie z zaleceniami producenta – zazwyczaj dwa razy do roku.
</div>
<h2 id="gwarancja-montaz">2. Gwarancja na usługę montażu</h2>
<p>
Bierzemy pełną odpowiedzialność za wykonaną przez nas pracę. Udzielamy <strong>5-letniej rękojmi</strong> na poprawność działania całej instalacji chłodniczej i odpływu skroplin. Jeśli jakikolwiek problem wynika z błędu montażowego, naprawimy go na własny koszt.
</p>
<h2 id="serwis">3. Autoryzowany Serwis</h2>
<p>
Posiadamy pełne zaplecze techniczne, własnych wykwalifikowanych serwisantów i uprawnienia UDT oraz certyfikaty F-GAZ. Co to oznacza dla Ciebie?
W przypadku awarii, nie odsyłamy Cię do producenta. Przyjeżdżamy na miejsce diagnozując usterkę, a w razie konieczności zamawiamy części i realizujemy naprawę, oszczędzając Twój czas i nerwy.
</p>
<h2 id="przeglady">4. Zleć nam przegląd gwarancyjny</h2>
<p>
Oferujemy atrakcyjne pakiety stałej opieki serwisowej. Przy stałej współpracy koszty serwisu są niższe, a my sami pilnujemy za Ciebie terminów obowiązkowych przeglądów narzucanych przez producentów sprzętu.
<br/><br/>
<Link href="/kontakt" className="font-bold text-accent hover:underline">Skontaktuj się z nami</Link>, aby zarezerwować termin wizyty serwisowej.
</p>
</>
);
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
export default function FormalneLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<main className="bg-slate-50 min-h-screen py-24 md:py-32">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="bg-white p-8 md:p-16 rounded-3xl border border-border shadow-sm prose prose-slate max-w-none prose-headings:font-heading prose-headings:text-primary prose-a:text-accent hover:prose-a:text-accent-hover prose-a:font-semibold">
{children}
</div>
</div>
</main>
);
}
@@ -0,0 +1,80 @@
import { Metadata } from 'next';
import { companyDetails } from '@/content/company';
export const metadata: Metadata = {
title: 'Polityka Prywatności',
description: 'Zasady przetwarzania danych osobowych oraz wykorzystania plików cookies w serwisie ThermCool.',
alternates: {
canonical: '/polityka-prywatnosci',
}
};
export default function PolitykaPrywatnosciPage() {
return (
<>
<h1 className="text-4xl font-extrabold mb-8">Polityka Prywatności i Cookies</h1>
<div className="prose prose-slate dark:prose-invert max-w-none space-y-6">
<p className="lead text-lg text-text-muted">
Poniższa Polityka Prywatności określa zasady zapisywania i uzyskiwania dostępu do danych na Urządzeniach Użytkowników korzystających z Serwisu do celów świadczenia usług drogą elektroniczną oraz zasady gromadzenia i przetwarzania danych osobowych Użytkowników.
</p>
<h2 id="administrator" className="text-2xl font-bold mt-8 mb-4">1. Administrator danych</h2>
<p>
Administratorem Twoich danych osobowych jest <strong>{companyDetails.legalName}</strong> z siedzibą we Wrocławiu (adres: {companyDetails.address.street}, {companyDetails.address.zipCode} {companyDetails.address.city}), NIP: {companyDetails.nip}, REGON: {companyDetails.regon}.
</p>
<p>Z administratorem można skontaktować się za pośrednictwem adresu e-mail: <strong>{companyDetails.email}</strong>.</p>
<h2 id="zakres-cel" className="text-2xl font-bold mt-8 mb-4">2. Zakres i cele przetwarzania</h2>
<p>Przetwarzamy dane osobowe podane przez Ciebie dobrowolnie w celu:</p>
<ul className="list-disc pl-6 space-y-2">
<li>Nawiązania kontaktu i odpowiedzi na przesłane zapytania (podstawa prawna: art. 6 ust. 1 lit. f RODO).</li>
<li>Realizacji usług, m.in. montażu, serwisu i dostarczenia urządzeń HVAC pod wskazany adres (podstawa prawna: art. 6 ust. 1 lit. b RODO).</li>
<li>Rozpatrywania potencjalnych reklamacji i roszczeń (podstawa prawna: art. 6 ust. 1 lit. c oraz f RODO).</li>
<li>Prowadzenia ksiąg rachunkowych i dokumentacji podatkowej.</li>
</ul>
<h2 id="prawa" className="text-2xl font-bold mt-8 mb-4">3. Prawa Użytkownika</h2>
<p>Zgodnie z przepisami RODO masz prawo do:</p>
<ul className="list-disc pl-6 space-y-2">
<li>Dostępu do swoich danych oraz otrzymania ich kopii.</li>
<li>Sprostowania (poprawienia) swoich danych.</li>
<li>Usunięcia danych (prawo do bycia zapomnianym) lub ograniczenia przetwarzania.</li>
<li>Przenoszenia danych do innego administratora.</li>
<li>Wniesienia sprzeciwu wobec przetwarzania danych w celach marketingowych lub wynikających z prawnie uzasadnionego interesu administratora.</li>
<li>Cofnięcia zgody w dowolnym momencie (bez wpływu na zgodność z prawem przetwarzania przed jej wycofaniem).</li>
</ul>
<h2 id="cookies" className="text-2xl font-bold mt-8 mb-4">4. Pliki Cookies i Analityka (Google Analytics 4 / Tag Manager)</h2>
<p>
Nasz serwis wykorzystuje pliki cookies (tzw. &quot;ciasteczka&quot;) – dane informatyczne, w szczególności pliki tekstowe, które przechowywane są w urządzeniu końcowym Użytkownika i przeznaczone są do korzystania z naszej strony internetowej.
</p>
<h3 className="text-xl font-semibold mt-6 mb-3">Google Analytics 4 (GA4) i Google Tag Manager (GTM)</h3>
<p>
W celu analizy statystyk ruchu, zrozumienia zachowań Użytkowników oraz ciągłej optymalizacji naszego serwisu korzystamy z narzędzi dostarczanych przez firmę Google LLC: <strong>Google Analytics 4</strong> oraz <strong>Google Tag Manager</strong>.
</p>
<ul className="list-disc pl-6 space-y-2 mt-4">
<li>
<strong>Zasada działania:</strong> Narzędzia te używają plików cookies do generowania anonimowych statystyk dotyczących sposobu korzystania z witryny. Generowane informacje są następnie przekazywane i przechowywane na serwerach Google.
</li>
<li>
<strong>Anonimizacja IP:</strong> Wdrożyliśmy funkcje ochrony prywatności, a informacje zbierane przez analitykę Google są pseudonimizowane, uniemożliwiając bezpośrednią identyfikację konkretnej osoby.
</li>
<li>
<strong>Zarządzanie:</strong> Użytkownik może w każdej chwili wyłączyć śledzenie analityczne za pomocą ustawień przeglądarki, instalując oficjalny dodatek do przeglądarki blokujący Google Analytics, lub korzystając z baneru zgody na naszej stronie.
</li>
</ul>
<h2 id="okres-przechowywania" className="text-2xl font-bold mt-8 mb-4">5. Okres przechowywania danych</h2>
<p>
Dane osobowe przetwarzane w celu zawarcia lub wykonania umowy będą przechowywane przez okres obowiązywania umowy, a po jego upływie przez okres niezbędny do posprzedażowej obsługi klientów (np. obsługi roszczeń z tytułu gwarancji/rękojmi) oraz wypełnienia obowiązków prawnych (np. rozliczeń podatkowych przez 5 lat).
</p>
<p className="text-sm text-text-muted mt-12 pt-8 border-t border-border">
Ostatnia aktualizacja dokumentu: {new Date().toLocaleDateString('pl-PL')} r.
</p>
</div>
</>
);
}
+64
View File
@@ -0,0 +1,64 @@
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Regulamin Sklepu i Usług',
description: 'Regulamin świadczenia usług, zasady korzystania ze sklepu, prawa i obowiązki klienta.',
alternates: {
canonical: '/regulamin',
}
};
export default function RegulaminPage() {
return (
<>
<h1 className="text-4xl font-extrabold mb-8">Regulamin Serwisu i Sklepu Internetowego</h1>
<p className="lead">
Niniejszy dokument określa zasady korzystania z serwisu AirProject oraz dokonywania w nim zakupów urządzeń i usług montażu.
</p>
{/* TODO: treść prawna do uzupełnienia przez właściciela i weryfikacji przez radcę prawnego */}
<h2 id="postanowienia-ogolne">§ 1. Postanowienia ogólne</h2>
<p>
1. Sklep internetowy działający pod adresem internetowym airproject.pl prowadzony jest przez:
<strong> [TUTAJ WPISZ DANE FIRMY: Nazwa, Adres, NIP, REGON]</strong>.
</p>
<p>
2. Niniejszy Regulamin określa zasady zawierania umów sprzedaży i świadczenia usług za pośrednictwem serwisu.
</p>
<h2 id="definicje">§ 2. Definicje</h2>
<ul>
<li><strong>Klient</strong> – osoba fizyczna, osoba prawna lub jednostka organizacyjna nie posiadająca osobowości prawnej.</li>
<li><strong>Konsument</strong> – Klient będący osobą fizyczną, dokonujący zakupów w celu niezwiązanym bezpośrednio z jego działalnością gospodarczą lub zawodową.</li>
<li><strong>Sklep</strong> – platforma e-commerce pod adresem airproject.pl.</li>
</ul>
<h2 id="zamowienia">§ 3. Zasady składania i realizacji zamówień</h2>
<p>
1. Informacje zawarte na Stronie Internetowej Sklepu nie stanowią oferty w rozumieniu Kodeksu Cywilnego, a jedynie zaproszenie do zawarcia umowy.
<br />
2. Klient może składać zamówienia 24 godziny na dobę przez 7 dni w tygodniu.
<br />
3. Realizacja usług montażowych odbywa się po uprzednim kontakcie i ustaleniu terminu.
</p>
<h2 id="platnosci">§ 4. Płatności i ceny</h2>
<p>
1. Wszystkie ceny na stronie są cenami brutto (zawierają podatek VAT).
</p>
<h2 id="odstapienie">§ 5. Odstąpienie od umowy</h2>
<p>
1. Konsument ma prawo odstąpić od umowy zawartej na odległość bez podania przyczyny w terminie 14 dni od dnia dostarczenia towaru.
<br />
2. Prawo odstąpienia nie przysługuje w odniesieniu do usług, które zostały w pełni wykonane za wyraźną zgodą Konsumenta.
</p>
<p className="text-sm text-text-muted mt-12 pt-8 border-t border-border">
Ostatnia aktualizacja regulaminu: 1 stycznia 2026 r.
</p>
</>
);
}
+64
View File
@@ -0,0 +1,64 @@
import { Metadata } from 'next';
import Link from 'next/link';
import { companyDetails } from '@/content/company';
export const metadata: Metadata = {
title: 'Reklamacje i Zwroty',
description: 'Zasady i procedury składania reklamacji oraz zwrotów produktów i usług w sklepie ThermCool.',
alternates: {
canonical: '/reklamacje',
}
};
export default function ReklamacjePage() {
return (
<>
<h1 className="text-4xl font-extrabold mb-8">Reklamacje i Zwroty</h1>
<p className="lead">
Dokładamy wszelkich starań, aby nasze produkty i usługi były najwyższej jakości. Jeśli jednak cokolwiek nie spełnia Twoich oczekiwań, masz pełne prawo złożyć reklamację lub dokonać zwrotu.
</p>
{/* TODO: treść prawna do uzupełnienia przez właściciela */}
<h2 id="zwroty">1. Zwroty (Odstąpienie od umowy)</h2>
<p>
Zgodnie z polskim prawem konsument ma prawo zwrócić zakupiony produkt bez podania przyczyny w ciągu 14 dni od dnia jego otrzymania. Pamiętaj jednak, że:
</p>
<ul>
<li>Zwracany towar nie może nosić śladów montażu i używania.</li>
<li>Koszty odesłania urządzenia ponosi kupujący.</li>
<li>W przypadku wykonania usługi montażu urządzenia za uprzednią zgodą konsumenta, prawo do odstąpienia od umowy może zostać wyłączone.</li>
</ul>
<h2 id="jak-zwrocic">Jak dokonać zwrotu?</h2>
<ol>
<li>Powiadom nas drogą mailową na adres: <strong>kontakt@twoj-sklep.pl</strong>.</li>
<li>Zapakuj bezpiecznie produkt w oryginalne opakowanie.</li>
<li>Odeślij go na wskazany przez nas adres magazynu.</li>
<li>Po weryfikacji sprzętu, środki zwrócimy Ci w ciągu 14 dni roboczych.</li>
</ol>
<h2 id="reklamacje">2. Procedura Reklamacyjna (Rękojmia i Gwarancja)</h2>
<p>
Wszystkie wady urządzeń mogą być zgłaszane z tytułu gwarancji producenta lub rękojmi.
Gwarancje na poszczególne urządzenia mogą wynosić od 2 do 5 lat (szczegóły w karcie produktu i zakładce <Link href="/gwarancja">Gwarancja</Link>).
</p>
<h2 id="jak-reklamowac">Jak zgłosić usterkę?</h2>
<p>
W razie awarii sprzętu zamontowanego przez naszą firmę, zalecamy kontakt telefoniczny lub poprzez e-mail:
</p>
<ul>
<li>Opisz rodzaj usterki i kod błędu wyświetlający się na urządzeniu (jeśli występuje).</li>
<li>Dołącz zdjęcia instalacji lub uszkodzonego elementu.</li>
<li>Nasz serwisant skontaktuje się z Tobą w celu ustalenia terminu darmowej diagnozy i naprawy (w okresie gwarancyjnym).</li>
</ul>
<div className="mt-8 p-6 bg-slate-50 border border-border rounded-xl">
<p className="mb-0 font-semibold text-primary">Masz dodatkowe pytania?</p>
<p className="mb-0 mt-2 text-sm">Zadzwoń do nas pod numer {companyDetails.phone} lub skorzystaj z <Link href="/kontakt">formularza kontaktowego</Link>.</p>
</div>
</>
);
}
+250
View File
@@ -0,0 +1,250 @@
import { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getPosts, getPostBySlug, getRelatedPosts } from '@/content/posts';
import { JsonLd } from '@/components/ui/JsonLd';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { Calendar, User, ArrowLeft, ArrowRight } from 'lucide-react';
import { companyDetails } from '@/content/company';
interface BlogPostPageProps {
params: Promise<{
slug: string;
}>;
}
// Generowanie ścieżek statycznych dla wpisów
export async function generateStaticParams() {
const posts = getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// Generowanie metadanych dla danego wpisu
export async function generateMetadata({ params }: BlogPostPageProps): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
return {
title: 'Nie znaleziono wpisu',
};
}
const pageTitle = post.metaTitle || post.title;
const pageDescription = post.metaDescription || post.excerpt;
return {
title: pageTitle,
description: pageDescription,
alternates: {
canonical: `/blog/${post.slug}`,
},
openGraph: {
title: pageTitle,
description: pageDescription,
type: 'article',
publishedTime: post.date,
authors: [post.author || companyDetails.shortName],
images: [
{
url: post.image || '/hero.jpg',
},
],
},
};
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
notFound();
}
const relatedPosts = getRelatedPosts(post.slug, post.category, 3);
const dateObj = new Date(post.date);
const formattedDate = dateObj.toLocaleDateString('pl-PL', {
day: 'numeric', month: 'long', year: 'numeric'
});
const categoryLabels: Record<string, string> = {
'klimatyzacja': 'Klimatyzacja',
'remonty': 'Remonty',
'hydraulika': 'Hydraulika',
'porady': 'Porady'
};
// Ustawienie CTA dopasowanego do kategorii wpisu
const getCtaLink = () => {
switch(post.category) {
case 'klimatyzacja': return '/klimatyzacja';
default: return '/kontakt';
}
};
// --- JSON-LD ---
const jsonLdArticle = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
image: post.image,
datePublished: post.date,
dateModified: post.date,
author: {
'@type': 'Person',
name: post.author || companyDetails.name,
},
publisher: {
'@type': 'Organization',
name: companyDetails.name,
logo: {
'@type': 'ImageObject',
url: 'https://thermcool.pl/logo.png', // MOCK URL
},
},
description: post.excerpt,
};
const jsonLdBreadcrumb = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Strona Główna',
item: 'https://thermcool.pl',
},
{
'@type': 'ListItem',
position: 2,
name: 'Blog',
item: 'https://thermcool.pl/blog',
},
{
'@type': 'ListItem',
position: 3,
name: post.title,
item: `https://thermcool.pl/blog/${post.slug}`,
},
],
};
return (
<main className="bg-slate-50 pt-24 pb-16">
{/* Skrypty JSON-LD dla SEO */}
<JsonLd data={jsonLdArticle} />
<JsonLd data={jsonLdBreadcrumb} />
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Powrót */}
<Link
href="/blog"
className="inline-flex items-center gap-2 text-sm font-semibold text-primary/70 hover:text-primary transition-colors mb-8"
>
<ArrowLeft className="w-4 h-4" />
Wróć do listy artykułów
</Link>
{/* Header artykułu */}
<header className="mb-10 text-center md:text-left">
<div className="flex flex-wrap items-center justify-center md:justify-start gap-4 mb-6 text-sm font-medium text-text-muted">
<span className="px-3 py-1 bg-accent/10 text-accent font-bold rounded-full uppercase tracking-wider">
{categoryLabels[post.category] || post.category}
</span>
<div className="flex items-center gap-1.5">
<Calendar className="w-4 h-4" />
<time dateTime={post.date}>{formattedDate}</time>
</div>
{post.author && (
<div className="flex items-center gap-1.5">
<User className="w-4 h-4" />
<span>{post.author}</span>
</div>
)}
</div>
<h1 className="text-3xl md:text-5xl font-extrabold text-primary font-heading leading-tight mb-6">
{post.title}
</h1>
<p className="text-lg md:text-xl text-text-muted leading-relaxed max-w-3xl">
{post.excerpt}
</p>
</header>
{/* Główny obraz */}
<div className="relative w-full aspect-video rounded-2xl overflow-hidden mb-12 shadow-md">
<Image
src={post.image || '/images/hero/hero-section-image.webp'}
alt={post.title}
fill
priority
className="object-cover"
/>
</div>
{/* Treść */}
<div className="prose prose-lg md:prose-xl prose-slate max-w-none mb-16
prose-headings:font-heading prose-headings:text-primary prose-headings:font-bold
prose-a:text-accent hover:prose-a:text-accent-hover prose-a:font-semibold
prose-img:rounded-xl prose-img:shadow-md
marker:text-accent"
>
{/* Dane blogowe są obecnie lokalnym, zaufanym contentem; nie dodajemy ciężkiej biblioteki DOMPurify bez potrzeby.
Przy wdrożeniu CMS lub wyświetlaniu treści od użytkowników wymagane będzie sanitizowanie HTML na serwerze. */}
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
</article>
{/* CTA do usługi */}
<CtaBanner
title="Podobał Ci się ten artykuł?"
subtitle="Jeśli potrzebujesz pomocy specjalistów w podobnej sprawie, sprawdź naszą ofertę lub skontaktuj się z nami bezpośrednio."
ctaText="Zobacz naszą ofertę"
ctaLink={getCtaLink()}
/>
{/* Powiązane artykuły */}
{relatedPosts.length > 0 && (
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-16 md:mt-24">
<div className="flex items-center justify-between border-b border-border pb-4 mb-8">
<h2 className="text-2xl font-bold text-primary font-heading">Powiązane artykuły</h2>
<Link href="/blog" className="text-sm font-semibold text-accent hover:text-accent-hover flex items-center gap-1">
Więcej wpisów <ArrowRight className="w-4 h-4" />
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{relatedPosts.map(related => (
<Link key={related.slug} href={`/blog/${related.slug}`} className="group block">
<div className="relative aspect-[16/10] rounded-xl overflow-hidden mb-4">
<Image
src={related.image || '/images/hero/hero-section-image.webp'}
alt={related.title}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
</div>
<h3 className="text-lg font-bold text-primary font-heading group-hover:text-accent transition-colors mb-2 line-clamp-2">
{related.title}
</h3>
<p className="text-sm text-text-muted line-clamp-2">
{related.excerpt}
</p>
</Link>
))}
</div>
</section>
)}
</main>
);
}
+49
View File
@@ -0,0 +1,49 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { PostsGrid } from '@/components/sections/PostsGrid';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { getPosts, getFeaturedPost } from '@/content/posts';
export const metadata: Metadata = {
title: 'Blog i Porady',
description: 'Baza wiedzy na temat klimatyzacji i pomp ciepła na Dolnym Śląsku. Poradniki, porównania urządzeń i aktualności branżowe.',
alternates: {
canonical: '/blog',
}
};
export const revalidate = 86400; // Regenerate every 24 hours
export default function BlogPage() {
const allPosts = getPosts();
const featuredPost = getFeaturedPost();
// Usuwamy z głównej siatki artykuł wyróżniony, żeby się nie powielał (chyba że chcemy, ale zazwyczaj się usuwa)
const remainingPosts = allPosts.filter(post => post.slug !== featuredPost.slug);
return (
<main>
<HeroSection
eyebrow="Baza Wiedzy"
title="Blog i poradniki"
subtitle="Dzielimy się naszą wiedzą i doświadczeniem. Zobacz, jak dbać o domowe instalacje, na co uważać podczas remontu i jak uzyskać dofinansowanie."
ctaText="Skontaktuj się z nami"
ctaLink="/kontakt"
secondaryCtaText="Nasze usługi"
secondaryCtaLink="/uslugi-domowe"
backgroundImage="/images/hero/hero-section-image.webp"
/>
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<PostsGrid posts={remainingPosts} featuredPost={featuredPost} />
</section>
<CtaBanner
title="Potrzebujesz indywidualnej porady?"
subtitle="Zadaj nam pytanie, a z przyjemnością udzielimy darmowej konsultacji i doradzimy najlepsze rozwiązanie."
ctaText="Zadaj pytanie"
ctaLink="/kontakt"
/>
</main>
);
}
+268
View File
@@ -0,0 +1,268 @@
import type { Metadata } from 'next';
import { JsonLd } from '@/components/ui/JsonLd';
import Link from 'next/link';
import Image from 'next/image';
import { Check, Phone, ArrowRight, Wrench, Zap, Wind } from 'lucide-react';
import { companyDetails } from '@/content/company';
import { buttonClasses, Button } from '@/components/ui/Button';
export const metadata: Metadata = {
title: 'Cennik montażu klimatyzacji Wrocław 2026 — ile kosztuje montaż?',
description: 'Sprawdź aktualne ceny montażu klimatyzacji we Wrocławiu. Montaż split od 1200 zł, bruzda 90 zł/mb, korytko 80 zł/mb. Bezpłatna wycena przed realizacją.',
alternates: { canonical: '/cennik-montazu' },
openGraph: {
title: 'Cennik montażu klimatyzacji Wrocław 2026',
description: 'Aktualne ceny montażu klimatyzacji. Split od 1200 zł, multisplit od 2800 zł. Wrocław i Dolny Śląsk.',
},
};
const faqItems = [
{
q: 'Ile kosztuje montaż klimatyzacji we Wrocławiu?',
a: 'Montaż klimatyzatora split w jednym pomieszczeniu kosztuje od 1200 zł netto. Cena obejmuje montaż jednostki wewnętrznej i zewnętrznej, wykonanie trasy freonowej do 3 mb, podłączenie elektryczne i uruchomienie. Każdy metr trasy powyżej 3 mb to dodatkowe 80 zł (korytko) lub 90 zł (bruzda w ścianie).',
},
{
q: 'Co jest droższe — bruzda czy korytko?',
a: 'Bruzda w ścianie kosztuje 90 zł/mb i daje estetyczniejszy efekt — rury są niewidoczne. Korytko PCV to 80 zł/mb, jest tańsze i szybsze w montażu, ale rury są widoczne wzdłuż ściany. Przy nowym budownictwie lub remoncie zdecydowanie polecamy bruzdę. W blokach z gotowym wykończeniem korytko jest najczęstszym wyborem.',
},
{
q: 'Czy cena montażu zawiera urządzenie?',
a: 'Nie — ceny w cenniku to wyłącznie koszt robocizny i montażu. Klimatyzator możesz kupić u nas w sklepie lub dostarczyć własny. Jeśli kupujesz u nas, możemy zaproponować pakiet urządzenie + montaż w atrakcyjnej cenie.',
},
{
q: 'Czy do ceny doliczacie VAT?',
a: 'Tak, wszystkie ceny w cenniku są cenami netto. Do podanych kwot doliczamy VAT 23%. Wystawiamy faktury VAT — klimatyzacja w firmie to koszt uzyskania przychodu.',
},
{
q: 'Dlaczego montaż na dachu lub poddaszu jest droższy?',
a: 'Montaż w trudno dostępnych miejscach wymaga dodatkowego sprzętu (drabiny, rusztowania), więcej czasu pracy ekipy i często specjalistycznych uchwytów montażowych. Dlatego takie realizacje wyceniamy indywidualnie po oględzinach.',
},
{
q: 'Jak uzyskać dokładną wycenę montażu?',
a: 'Zadzwoń do nas lub wypełnij formularz kontaktowy. Przyjedziemy na bezpłatne oględziny, ocenimy warunki montażu i w ciągu 24 godzin wystawimy szczegółową wycenę. Cena w wycenie jest ceną ostateczną — bez ukrytych kosztów.',
},
];
const CENNIK_MONTAZ = [
{ label: 'Montaż klimatyzatora split — podstawowy (do 4,5 kW, trasa do 3 mb)', cena: '1400 zł netto', highlight: true },
{ label: 'Montaż klimatyzatora split przy istniejącej instalacji', cena: '800 zł netto', highlight: false },
{ label: 'Montaż multisplit 2+1 — podstawowy (do 6 mb instalacji)', cena: '2400 zł netto', highlight: false },
{ label: 'Montaż multisplit 3+1 — podstawowy (do 9 mb instalacji)', cena: '3200 zł netto', highlight: false },
{ label: 'Montaż multisplit 4+1 — podstawowy (do 12 mb instalacji)', cena: '3800 zł netto', highlight: false },
{ label: 'Montaż multisplit powyżej 4 jednostek wewnętrznych', cena: 'wycena indywidualna', highlight: false },
{ label: 'Przegląd gwarancyjny klimatyzatora split', cena: '230 zł netto', highlight: false },
{ label: 'Przegląd gwarancyjny multisplit 2+1', cena: '250 zł netto', highlight: false },
{ label: 'Przegląd gwarancyjny multisplit 3+1 / 4+1 / 5+1', cena: '700 zł netto', highlight: false },
];
const CENNIK_TRASA = [
{ label: 'Dodatkowy metr instalacji freonowej (korytko)', cena: '80 zł/mb' },
{ label: 'Dodatkowy metr instalacji freonowej (bruzda)', cena: '90 zł/mb' },
{ label: 'Dodatkowy metr instalacji elektrycznej', cena: '20 zł/mb' },
{ label: 'Dopłata za przewiert otworu instalacyjnego w betonie', cena: '100 zł' },
{ label: 'Dopłata za kucie bruzdy instalacyjnej w żelbecie', cena: '150 zł' },
{ label: 'Instalacja pompy skroplin', cena: '400 zł' },
{ label: 'Instalacja jednostki zewnętrznej z rusztowania', cena: 'wycena indywidualna' },
{ label: 'Instalacja jednostki zewnętrznej ze zwyżki', cena: 'wycena indywidualna' },
{ label: 'Dopłata do montażu poza sezonem (1 sobota/dzień)', cena: '240 zł' },
{ label: 'Demontaż klimatyzacji', cena: '300 zł netto' },
];
const CENNIK_SERWIS = [
{ label: 'Czyszczenie i odgrzybianie klimatyzatora', cena: 'od 250 zł' },
{ label: 'Przegląd techniczny (F-Gaz)', cena: 'od 150 zł' },
{ label: 'Uzupełnienie czynnika R32', cena: 'od 180 zł' },
{ label: 'Uzupełnienie czynnika R410A', cena: 'od 220 zł' },
{ label: 'Diagnostyka i naprawa', cena: 'wycena po diagnostyce' },
{ label: 'Serwis sezonowy / abonament', cena: 'od 199 zł/rok' },
];
export default function CennikMontazuPage() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Service',
name: 'Montaż klimatyzacji Wrocław — cennik',
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: 'Cennik montażu klimatyzacji we Wrocławiu i na Dolnym Śląsku. Montaż split od 1200 zł, bruzda 90 zł/mb, korytko 80 zł/mb.',
offers: [
{ '@type': 'Offer', name: 'Montaż klimatyzatora split', priceCurrency: 'PLN', price: '1200' },
{ '@type': 'Offer', name: 'Czyszczenie klimatyzatora', priceCurrency: 'PLN', price: '250' },
],
mainEntityOfPage: {
'@type': 'FAQPage',
mainEntity: faqItems.map(f => ({
'@type': 'Question',
name: f.q,
acceptedAnswer: { '@type': 'Answer', text: f.a },
})),
},
};
return (
<>
<JsonLd data={jsonLd} />
<main>
{/* Hero */}
<section className="relative text-white py-24 md:py-32 overflow-hidden">
<Image
src="/images/services/wycena.webp"
alt="Cennik montażu klimatyzacji Wrocław"
fill
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-primary/50" />
<div className="relative z-10 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" />
Aktualne ceny — 2026
</div>
<h1 className="text-3xl md:text-5xl font-extrabold font-heading leading-tight mb-6">
Cennik montażu klimatyzacji<br />
<span className="text-accent">Wrocław — bez ukrytych kosztów</span>
</h1>
<p className="text-lg text-white/80 mb-8 max-w-2xl">
Przejrzyste ceny montażu klimatyzacji. Przed każdą realizacją wystawiamy
bezpłatną wycenę — płacisz dokładnie tyle ile uzgodniono. Zero niespodzianek.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="md">
<Phone className="size-5" /> Zadzwoń po wycenę
</Button>
</a>
<Link href="/kontakt" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Formularz kontaktowy <ArrowRight className="size-5" />
</Link>
</div>
</div>
</section>
{/* Cennik główny */}
<section className="py-16 bg-white">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col gap-10">
{/* Montaż */}
<div>
<div className="flex items-center gap-2 mb-4">
<Wrench className="size-5 text-accent" />
<h2 className="text-xl font-extrabold text-primary font-heading">Montaż klimatyzacji</h2>
</div>
<div className="rounded-2xl border border-border overflow-hidden">
{CENNIK_MONTAZ.map((r, i) => (
<div key={i} className={`flex items-center justify-between px-5 py-4 gap-4 ${i > 0 ? 'border-t border-border' : ''} ${r.highlight ? 'bg-accent/5' : 'bg-white'}`}>
<div className="flex items-center gap-2">
{r.highlight && <Check className="size-4 text-accent shrink-0" />}
<span className={`text-sm ${r.highlight ? 'font-semibold text-primary' : 'text-primary'}`}>{r.label}</span>
</div>
<span className={`text-sm font-bold whitespace-nowrap ${r.highlight ? 'text-accent' : 'text-primary'}`}>{r.cena}</span>
</div>
))}
</div>
</div>
{/* Trasa freonowa */}
<div>
<div className="flex items-center gap-2 mb-4">
<Zap className="size-5 text-accent" />
<h2 className="text-xl font-extrabold text-primary font-heading">Trasa freonowa i dodatkowe prace</h2>
</div>
<div className="rounded-2xl border border-border overflow-hidden bg-white">
{CENNIK_TRASA.map((r, i) => (
<div key={i} className={`flex items-center justify-between px-5 py-4 gap-4 ${i > 0 ? 'border-t border-border' : ''}`}>
<span className="text-sm text-primary">{r.label}</span>
<span className="text-sm font-bold text-primary whitespace-nowrap">{r.cena}</span>
</div>
))}
</div>
<div className="mt-4 p-4 bg-amber-50 border border-amber-200 rounded-xl">
<p className="text-xs text-amber-900 leading-relaxed">
<span className="font-bold">Bruzda vs korytko:</span> Bruzda w ścianie (90 zł/mb) daje niewidoczną instalację — polecana przy nowym budownictwie i remontach. Korytko PCV (80 zł/mb) jest szybsze i tańsze — popularne w gotowych mieszkaniach. Doradzimy najlepsze rozwiązanie po oględzinach.
</p>
</div>
</div>
{/* Serwis */}
<div>
<div className="flex items-center gap-2 mb-4">
<Wind className="size-5 text-accent" />
<h2 className="text-xl font-extrabold text-primary font-heading">Serwis i przeglądy</h2>
</div>
<div className="rounded-2xl border border-border overflow-hidden bg-white">
{CENNIK_SERWIS.map((r, i) => (
<div key={i} className={`flex items-center justify-between px-5 py-4 gap-4 ${i > 0 ? 'border-t border-border' : ''}`}>
<span className="text-sm text-primary">{r.label}</span>
<span className="text-sm font-bold text-primary whitespace-nowrap">{r.cena}</span>
</div>
))}
</div>
</div>
<p className="text-xs text-text-muted">
* Wszystkie ceny są cenami netto. Do podanych kwot doliczany jest VAT 23%.
Ostateczna wycena po oględzinach miejsca montażu. Cena w zatwierdzonej wycenie jest ceną ostateczną.
</p>
</div>
</section>
{/* FAQ */}
<section className="py-16 bg-slate-50">
<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 o cennik</h2>
<div className="flex flex-col gap-4">
{faqItems.map((item, i) => (
<details key={i} className="group bg-white 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">
Chcesz poznać dokładną cenę montażu?
</h2>
<p className="text-white/80 mb-8">
Zadzwoń lub napisz — bezpłatna wycena w ciągu 24 godzin. Obsługujemy Wrocław i cały Dolny Śląsk.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="md">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
<Link href="/kontakt" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Napisz do nas <ArrowRight className="size-5" />
</Link>
</div>
</div>
</section>
</main>
</>
);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

+224
View File
@@ -0,0 +1,224 @@
@import "tailwindcss";
@plugin "daisyui";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-text: var(--foreground);
--color-text-muted: var(--muted-foreground);
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-heading: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-border: var(--theme-border);
--color-input: var(--input);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-accent-light: var(--accent-light);
--color-cta: var(--cta);
--color-cta-foreground: var(--cta-foreground);
--color-cta-hover: var(--cta-hover);
--color-primary-light: var(--primary-light);
--color-success: var(--success);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
:root {
--background: #ffffff;
--foreground: #1a2230;
--card: #ffffff;
--card-foreground: #1a2230;
--popover: #ffffff;
--popover-foreground: #1a2230;
--primary: #0f2a47;
--primary-foreground: #ffffff;
--primary-light: #1b3d63;
--secondary: #f5f7fa;
--secondary-foreground: #0f2a47;
--muted: #f5f7fa;
--muted-foreground: #5a6678;
--accent: #1030D0;
--accent-foreground: #ffffff;
--accent-light: #e3f4fc;
--cta: #F39320;
--cta-foreground: #ffffff;
--cta-hover: #D97D0D;
--destructive: oklch(0.577 0.245 27.325);
--theme-border: #e2e8f0;
--input: #e2e8f0;
--ring: #1030D0;
--success: #16a34a;
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.75rem;
--sidebar: #ffffff;
--sidebar-foreground: #1a2230;
--sidebar-primary: #0f2a47;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #f5f7fa;
--sidebar-accent-foreground: #0f2a47;
--sidebar-border: #e2e8f0;
--sidebar-ring: #1030D0;
/* DaisyUI Overrides */
--color-primary: var(--accent);
--color-primary-content: #ffffff;
--color-secondary: var(--secondary);
--color-secondary-content: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-content: var(--accent-foreground);
--color-neutral: #1a2230;
--color-neutral-content: #ffffff;
--color-base-100: var(--background);
--color-base-200: var(--muted);
--color-base-300: var(--theme-border);
--color-base-content: var(--foreground);
--color-success: var(--success);
--color-error: var(--destructive);
--radius-selector: var(--radius);
--radius-field: var(--radius);
--radius-box: var(--radius);
}
.dark {
--background: #0f2a47;
--foreground: #f5f7fa;
--card: #1b3d63;
--card-foreground: #f5f7fa;
--popover: #1b3d63;
--popover-foreground: #f5f7fa;
--primary: #2ba8e0;
--primary-foreground: #0f2a47;
--secondary: #1b3d63;
--secondary-foreground: #f5f7fa;
--muted: #1b3d63;
--muted-foreground: #e3f4fc;
--accent: #2ba8e0;
--accent-foreground: #0f2a47;
--destructive: oklch(0.704 0.191 22.216);
--theme-border: rgba(226, 232, 240, 0.1);
--input: rgba(226, 232, 240, 0.15);
--ring: #2ba8e0;
/* DaisyUI Overrides */
--color-primary: var(--accent);
--color-primary-content: #ffffff;
--color-secondary: var(--secondary);
--color-secondary-content: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-content: var(--accent-foreground);
--color-neutral: #1a2230;
--color-neutral-content: #ffffff;
--color-base-100: var(--background);
--color-base-200: var(--muted);
--color-base-300: var(--theme-border);
--color-base-content: var(--foreground);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
*:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
*, ::before, ::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
body {
@apply bg-background text-foreground overflow-x-hidden;
}
html {
@apply font-sans overflow-x-hidden scroll-smooth;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-heading;
}
}
@layer components {
.btn {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 4px 6px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease-in-out;
}
.btn:hover {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.08), 0 10px 15px rgba(0, 0, 0, 0.05);
transform: translateY(-1px);
}
.btn.btn-outline.bg-white {
background-color: #ffffff !important;
color: var(--accent) !important;
border-color: var(--accent) !important;
}
.btn.btn-outline.bg-white:hover {
background-color: var(--accent) !important;
color: #ffffff !important;
border-color: var(--accent) !important;
}
}
@layer utilities {
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(calc(-50% - 0.5rem)); }
}
.animate-marquee {
animation: marquee 25s linear infinite;
}
.animate-marquee:hover {
animation-play-state: paused;
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

+13
View File
@@ -0,0 +1,13 @@
import type { Metadata } from 'next';
import { KalkulatorClient } from '@/components/kalkulator/KalkulatorClient';
import { getProducts } from '@/content/products';
export const metadata: Metadata = {
title: "Kalkulator doboru klimatyzacji",
description: 'Dobierz klimatyzator do swojego pomieszczenia w 4 krokach. Kalkulator mocy, dobór kategorii i szacunkowy koszt montażu.',
};
export default function KalkulatorPage() {
const products = getProducts();
return <KalkulatorClient products={products} />;
}
+173
View File
@@ -0,0 +1,173 @@
import { Metadata } from 'next';
import { JsonLd } from '@/components/ui/JsonLd';
import { HeroSection } from '@/components/sections/HeroSection';
import { InfoHighlights } from '@/components/sections/InfoHighlights';
import { AcTypesSection } from '@/components/sections/AcTypesSection';
import { ProcessSteps } from '@/components/sections/ProcessSteps';
import { ConfiguratorTeaser } from '@/components/sections/ConfiguratorTeaser';
import { PricingTable } from '@/components/sections/PricingTable';
import { BrandsGrid } from '@/components/sections/BrandsGrid';
import { FeaturedProducts } from '@/components/sections/FeaturedProducts';
import { WhyUs } from '@/components/sections/WhyUs';
import { FaqAccordion } from '@/components/sections/FaqAccordion';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { acProcessSteps } from '@/content/process';
import { dolnySlaskCities } from '@/content/cities';
import Link from 'next/link';
export const metadata: Metadata = {
title: 'Montaż Klimatyzacji Wrocław i Dolny Śląsk',
description: 'Profesjonalny montaż klimatyzacji split i multisplit we Wrocławiu i na Dolnym Śląsku. Uprawnienia F-Gaz, bezpłatna wycena, estetyczny montaż.',
alternates: {
canonical: '/klimatyzacja',
}
};
export default function KlimatyzacjaPage() {
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Ile kosztuje montaż klimatyzacji?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Koszt montażu klimatyzacji split wynosi od 1200 zł netto. Cena zależy od długości instalacji, trudności montażu i wybranego urządzenia. Oferujemy bezpłatną wycenę przed realizacją.",
},
},
{
"@type": "Question",
"name": "Jak długo trwa montaż klimatyzacji?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Standardowy montaż klimatyzatora split trwa od 4 do 8 godzin. Systemy multisplit wymagają 1-2 dni roboczych w zależności od liczby jednostek.",
},
},
{
"@type": "Question",
"name": "Czy potrzebuję uprawnień F-Gaz do montażu klimatyzacji?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Tak, montaż klimatyzacji zawierającej czynniki F-Gaz (R32, R410A) wymaga certyfikatu F-Gaz. Posiadamy wszystkie wymagane uprawnienia UDT.",
},
},
{
"@type": "Question",
"name": "Jak często serwisować klimatyzację?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Zalecamy serwis klimatyzacji 2 razy w roku: przed sezonem letnim (kwiecień-maj) i przed zimowym (wrzesień-październik). Regularny serwis zachowuje gwarancję producenta.",
},
},
{
"@type": "Question",
"name": "Czy klimatyzacja może ogrzewać pomieszczenia zimą?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Tak, nowoczesne klimatyzatory inwerterowe to pompy ciepła powietrze-powietrze, które efektywnie ogrzewają nawet przy -20°C na zewnątrz. Koszt ogrzewania klimatyzacją jest 3-4 razy niższy niż ogrzewaniem elektrycznym.",
},
},
],
};
return (
<>
<JsonLd data={faqJsonLd} />
<main>
<HeroSection
eyebrow="15+ lat doświadczenia · Split i multisplit · Uprawnienia F-gaz"
title="Montaż klimatyzacji — dobór, instalacja, serwis"
subtitle="Zapewnij sobie idealny klimat w domu i biurze. Oferujemy kompleksowe usługi od bezpłatnego audytu po montaż i opiekę serwisową."
ctaText="Dobierz klimatyzator"
ctaLink="/sklep"
secondaryCtaText="Sprawdź cennik"
secondaryCtaLink="#cennik"
backgroundImage="/images/services/montaz.webp"
features={[
'Bezpłatny audyt',
'Estetyczny montaż',
'Zgodność z F-gaz',
'Serwis gwarancyjny'
]}
/>
<InfoHighlights
description="Montaż klimatyzacji to inwestycja w komfort na lata. Dlatego kładziemy nacisk na precyzję, estetykę wykonania i czystość w miejscu pracy. Nasza ekipa używa profesjonalnych narzędzi, a każdy montaż kończymy szkoleniem z obsługi."
highlights={[
{
icon: 'clock',
title: 'Czas montażu',
description: 'Standardowy montaż jednostki typu split trwa zazwyczaj od 4 do 8 godzin.'
},
{
icon: 'price',
title: 'Ceny od 1200 zł',
description: 'Jasny cennik usług bez ukrytych opłat. Ostateczna wycena zawsze przed rozpoczęciem prac.'
},
{
icon: 'map',
title: 'Zasięg działania',
description: 'Świadczymy usługi na terenie miasta i w promieniu 50 km z bezpłatnym dojazdem.'
}
]}
/>
<AcTypesSection />
<ProcessSteps
steps={acProcessSteps}
eyebrow="Krok po Kroku"
title="Jak wygląda proces montażu?"
subtitle="Od pierwszej wizyty do pełnego uruchomienia — zobacz, jak działamy u naszych klientów."
/>
<ConfiguratorTeaser />
<PricingTable />
<BrandsGrid />
<FeaturedProducts />
<WhyUs />
<FaqAccordion category="klimatyzacja" />
{/* Sekcja miast dla SEO */}
<section id="miasta" className="py-16 bg-slate-50 border-t border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-10">
<span className="text-xs font-semibold uppercase tracking-wider text-accent font-heading">
Zasięg działania
</span>
<h2 className="text-2xl md:text-3xl font-bold text-primary font-heading mt-2">
Montaż klimatyzacji na Dolnym Śląsku
</h2>
<p className="text-text-muted mt-3 max-w-xl mx-auto">
Działamy na terenie całego Dolnego Śląska. Wybierz swoje miasto i dowiedz się więcej:
</p>
</div>
<div className="flex flex-wrap justify-center gap-3">
{dolnySlaskCities.map((city) => (
<Link
key={city.slug}
href={`/klimatyzacja/${city.slug}`}
className="px-4 py-2 rounded-full bg-white border border-border text-sm font-medium text-primary hover:border-accent hover:text-accent transition-colors shadow-sm"
>
Klimatyzacja {city.name}
</Link>
))}
</div>
</div>
</section>
<CtaBanner
title="Niestandardowa instalacja?"
subtitle="Potrzebujesz klimatyzacji w zabytkowym budynku lub masz specyficzne wymagania architektoniczne? Skontaktuj się z nami po indywidualną wycenę."
ctaText="Zamów wycenę"
ctaLink="/kontakt"
/>
</main>
</>
);
}
+137
View File
@@ -0,0 +1,137 @@
"use server";
import { z } from "zod";
import { escapeHtml, rateLimit, verifyRecaptcha } from "@/lib/security";
import { getMailClient } from "@/lib/mail";
import { headers } from "next/headers";
const contactFormSchema = z.object({
name: z.string().min(2, "Imię musi mieć min. 2 znaki"),
phone: z.string().min(9, "Wprowadź poprawny numer telefonu"),
email: z.union([z.literal(''), z.string().email("Niepoprawny adres e-mail")]).optional(),
city: z.string().min(1, "Wybierz miasto"),
subject: z.string().optional(),
message: z.string().optional(),
consent: z.boolean().refine((val) => val === true, "Zgoda RODO jest wymagana"),
});
export async function submitContactForm(prevState: unknown, formData: FormData) {
try {
const ip = (await headers()).get('x-forwarded-for') || 'unknown';
if (!rateLimit(ip)) {
return { success: false, message: 'Wysłano zbyt wiele zapytań. Spróbuj ponownie później.' };
}
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && process.env.RECAPTCHA_SECRET_KEY) {
const token = formData.get('recaptchaToken') as string;
if (!token) {
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
}
const success = await verifyRecaptcha(token);
if (!success) {
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
}
}
const rawData = {
name: formData.get("name"),
phone: formData.get("phone"),
email: formData.get("email"),
city: formData.get("city"),
subject: formData.get("subject"),
message: formData.get("message"),
consent: formData.get("consent") === "true" || formData.get("consent") === "on",
};
const validatedData = contactFormSchema.parse(rawData);
const mailClient = getMailClient();
if (!mailClient) {
return {
success: false,
message: "Funkcja wysyłania wiadomości jest tymczasowo niedostępna.",
};
}
// 2. Wysłanie e-maila przez Resend
const { data, error } = await mailClient.resend.emails.send({
from: process.env.RESEND_FROM_EMAIL || "[email protected]",
to: process.env.RESEND_TO_EMAIL || "[email protected]", // Docelowy e-mail (odbiorca)
replyTo: validatedData.email,
subject: `Nowa wiadomość ze strony: ${validatedData.subject}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; background-color: #ffffff;">
<div style="background-color: #0F2A47; padding: 20px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px;">Nowe zapytanie z formularza</h1>
<p style="color: #2BA8E0; margin: 5px 0 0 0; font-size: 14px;">Domena: thermcool.pl</p>
</div>
<div style="padding: 24px; color: #1e293b;">
<h2 style="font-size: 18px; margin-top: 0; border-bottom: 2px solid #f1f5f9; padding-bottom: 10px;">Dane kontaktowe:</h2>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9; width: 120px;"><strong>Imię:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.name)}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Telefon:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><a href="tel:${escapeHtml(validatedData.phone)}" style="color: #0F2A47; text-decoration: none;">${escapeHtml(validatedData.phone)}</a></td>
</tr>
<tr>
<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:${escapeHtml(validatedData.email || '')}" style="color: #0F2A47; text-decoration: none;">${escapeHtml(validatedData.email || '')}</a></td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Miasto:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.city)}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Temat:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.subject || '')}</td>
</tr>
</table>
<h2 style="font-size: 18px; border-bottom: 2px solid #f1f5f9; padding-bottom: 10px;">Treść wiadomości:</h2>
<div style="background-color: #f8fafc; padding: 15px; border-radius: 6px; font-size: 15px; line-height: 1.6; white-space: pre-wrap;">
${escapeHtml(validatedData.message || '')}
</div>
<p style="margin-top: 24px; font-size: 12px; color: #64748b; text-align: center;">
Zgoda RODO została zaakceptowana: <strong>${validatedData.consent ? "Tak" : "Nie"}</strong>
</p>
</div>
<div style="background-color: #f1f5f9; padding: 15px; text-align: center; font-size: 12px; color: #64748b;">
Wiadomość wygenerowana automatycznie z formularza kontaktowego na stronie thermcool.pl
</div>
</div>
`,
});
if (error) {
console.error("Resend Error:", error);
return {
success: false,
message: "Nie udało się wysłać wiadomości ze względu na błąd serwera pocztowego.",
};
}
console.log("Wysłano e-mail (ID):", data?.id);
return {
success: true,
message: "Dziękujemy za wiadomość! Skontaktujemy się z Tobą najszybciej jak to możliwe.",
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
message: "Wystąpił błąd walidacji. Sprawdź poprawność danych.",
errors: error.flatten().fieldErrors,
};
}
return {
success: false,
message: "Wystąpił nieoczekiwany błąd serwera. Spróbuj ponownie później.",
};
}
}
+34
View File
@@ -0,0 +1,34 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { ContactSection } from '@/components/sections/ContactSection';
import { companyDetails } from '@/content/company';
export const metadata: Metadata = {
title: 'Kontakt — Klimatyzacja Wrocław',
description: 'Skontaktuj się z nami! Odpowiemy na Twoje pytania, doradzimy w wyborze urządzeń i przygotujemy bezpłatną wycenę. Jesteśmy do Twojej dyspozycji.',
alternates: {
canonical: '/kontakt',
}
};
export default function KontaktPage() {
return (
<main>
<HeroSection
eyebrow="Kontakt"
title="Jesteśmy do Twojej dyspozycji"
subtitle="Zastanawiasz się nad montażem klimatyzacji ? Potrzebujesz serwisu? Napisz lub zadzwoń — odpowiemy szybko i bezpłatnie."
ctaText="Zadzwoń do nas"
ctaLink={`tel:${companyDetails.phoneRaw}`}
secondaryCtaText="Napisz maila"
secondaryCtaLink={`mailto:${companyDetails.email}`}
backgroundImage="/images/hero/hero-section-image.webp"
/>
<ContactSection />
</main>
);
}
+17
View File
@@ -0,0 +1,17 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Koszyk',
robots: {
index: false,
follow: true,
},
};
export default function KoszykLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}
+99
View File
@@ -0,0 +1,99 @@
'use client';
import { useCartStore } from '@/lib/cart-store';
import Image from 'next/image';
import Link from 'next/link';
import { Trash2, Plus, Minus, ShoppingBag, ArrowRight } from 'lucide-react';
import { buttonClasses } from '@/components/ui/Button';
export default function KoszykPage() {
const { items, removeItem, updateQuantity, getTotal, clearCart } = useCartStore();
const total = getTotal();
if (items.length === 0) {
return (
<main className="min-h-screen bg-slate-50 flex items-center justify-center">
<div className="text-center">
<ShoppingBag className="size-16 text-border mx-auto mb-4" />
<h2 className="text-2xl font-bold text-primary font-heading mb-2">Koszyk jest pusty</h2>
<p className="text-text-muted mb-6">Dodaj produkty ze sklepu aby złożyć zamówienie.</p>
<Link href="/sklep" className={buttonClasses({ variant: 'primary', size: 'md' })}>
Przejdź do sklepu
</Link>
</div>
</main>
);
}
return (
<main className="bg-slate-50 min-h-screen py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-bold text-primary font-heading mb-8">Twój koszyk</h1>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Lista produktów */}
<div className="lg:col-span-2 flex flex-col gap-4">
{items.map((item) => (
<div key={item.id} className="bg-white rounded-xl border border-border p-4 flex gap-4 items-center">
<div className="relative size-20 bg-slate-50 rounded-lg shrink-0">
<Image src={item.image} alt={item.name} fill className="object-contain p-2" unoptimized={item.image.startsWith('http')} />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-accent font-semibold">{item.brand}</p>
<p className="text-sm font-semibold text-primary font-heading leading-snug line-clamp-2">{item.name}</p>
<p className="text-base font-bold text-primary mt-1">{item.price.toLocaleString('pl-PL')} zł</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button onClick={() => updateQuantity(item.id, item.quantity - 1)} className="size-7 rounded-lg border border-border flex items-center justify-center text-primary hover:border-accent hover:text-accent transition-colors">
<Minus className="size-3.5" />
</button>
<span className="text-sm font-bold text-primary w-6 text-center">{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)} className="size-7 rounded-lg border border-border flex items-center justify-center text-primary hover:border-accent hover:text-accent transition-colors">
<Plus className="size-3.5" />
</button>
</div>
<p className="text-base font-bold text-primary shrink-0 w-24 text-right">
{(item.price * item.quantity).toLocaleString('pl-PL')} zł
</p>
<button onClick={() => removeItem(item.id)} className="text-text-muted hover:text-red-500 transition-colors shrink-0">
<Trash2 className="size-4" />
</button>
</div>
))}
</div>
{/* Podsumowanie */}
<div className="lg:col-span-1">
<div className="bg-white rounded-xl border border-border p-6 sticky top-24">
<h2 className="text-base font-bold text-primary font-heading mb-4">Podsumowanie</h2>
<div className="flex justify-between text-sm text-text-muted mb-2">
<span>Produkty ({items.reduce((s, i) => s + i.quantity, 0)} szt.)</span>
<span>{total.toLocaleString('pl-PL')} zł</span>
</div>
<div className="flex justify-between text-sm text-text-muted mb-4">
<span>Montaż</span>
<span className="text-accent font-medium">wycena telefoniczna</span>
</div>
<div className="border-t border-border pt-4 mb-6">
<div className="flex justify-between font-bold text-primary">
<span>Razem (urządzenia)</span>
<span>{total.toLocaleString('pl-PL')} zł</span>
</div>
<p className="text-xs text-text-muted mt-1">+ koszt montażu ustalany indywidualnie</p>
</div>
<Link href="/zamowienie" className="block btn btn-primary text-white font-bold w-full flex items-center justify-center gap-2">
Złóż zamówienie <ArrowRight className="size-4" />
</Link>
<button onClick={clearCart} className="w-full text-xs text-text-muted hover:text-primary transition-colors mt-3">
Wyczyść koszyk
</button>
</div>
</div>
</div>
</div>
</main>
);
}
+80
View File
@@ -0,0 +1,80 @@
import type { Metadata } from "next";
import { Header } from "@/components/layout/Header";
import { TrustBar } from "@/components/layout/TrustBar";
import { Footer } from "@/components/layout/Footer";
import CookieBanner from '@/components/CookieBanner';
import Script from 'next/script';
import { CompareBar } from '@/components/shop/CompareBar';
import { AnalyticsWrapper } from "@/components/ui/AnalyticsWrapper";
import { CookieSettingsFloatingIcon } from "@/components/ui/CookieSettingsFloatingIcon";
import "./globals.css";
export const metadata: Metadata = {
title: {
default: "ThermCool | Klimatyzacja Wrocław — Dolny Śląsk",
template: "%s | ThermCool",
},
description: "Profesjonalny montaż i serwis klimatyzacji na Dolnym Śląsku. Uprawnienia F-Gaz, bezpłatny audyt, estetyczny montaż. Zadzwoń po wycenę!",
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl'),
openGraph: {
type: 'website',
locale: 'pl_PL',
siteName: 'ThermCool',
title: 'ThermCool | Klimatyzacja Wrocław — Dolny Śląsk',
description: 'Profesjonalny montaż i serwis klimatyzacji oraz pomp ciepła na Dolnym Śląsku.',
},
twitter: {
card: 'summary_large_image',
title: 'ThermCool | Klimatyzacja ',
description: 'Profesjonalny montaż i serwis klimatyzacji oraz pomp ciepła na Dolnym Śląsku.',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="pl"
className="antialiased"
>
<head>
</head>
<body className="min-h-screen flex flex-col bg-background text-foreground">
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[9999] focus:px-4 focus:py-2 focus:bg-accent focus:text-white focus:rounded-md focus:font-bold">
Przejdź do głównej treści
</a>
{process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && (
<Script
src={`https://www.google.com/recaptcha/api.js?render=${process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}`}
strategy="afterInteractive"
/>
)}
<Header />
<main id="main-content" tabIndex={-1} className="flex-grow focus:outline-none">{children}</main>
<TrustBar />
<Footer />
<AnalyticsWrapper />
<CompareBar />
<CookieBanner />
<CookieSettingsFloatingIcon />
</body>
</html>
);
}
+7
View File
@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-muted">
<div className="w-16 h-16 bg-primary rounded-full animate-pulse shadow-md"></div>
</div>
);
}
+265
View File
@@ -0,0 +1,265 @@
import type { Metadata } from 'next';
import { JsonLd } from '@/components/ui/JsonLd';
import Link from 'next/link';
import Image from 'next/image';
import { Check, Phone, Wrench, ArrowRight, ShieldCheck, Clock, Star } from 'lucide-react';
import { companyDetails } from '@/content/company';
import { buttonClasses, Button } from '@/components/ui/Button';
export const metadata: Metadata = {
title: 'Montaż klimatyzacji Wrocław — split, multisplit',
description: 'Profesjonalny montaż klimatyzacji we Wrocławiu i na Dolnym Śląsku. Uprawnienia F-Gaz, estetyczna instalacja, gwarancja 2 lata. Bezpłatna wycena — zadzwoń!',
alternates: { canonical: '/montaz-klimatyzacji' },
openGraph: {
title: 'Montaż klimatyzacji Wrocław',
description: 'Montaż klimatyzatorów split i multisplit z uprawnieniami F-Gaz. Wrocław i Dolny Śląsk. Bezpłatna wycena.',
},
};
const faqItems = [
{
q: 'Ile kosztuje montaż klimatyzacji we Wrocławiu?',
a: 'Koszt montażu klimatyzatora split zaczyna się od 1200 zł i obejmuje montaż jednostki wewnętrznej i zewnętrznej, wykonanie trasy freonowej do 3 mb, podłączenie elektryczne i uruchomienie. Montaż multisplit (kilka pomieszczeń) wyceniamy indywidualnie. Przed każdą realizacją wystawiamy bezpłatną wycenę.',
},
{
q: 'Jak długo trwa montaż klimatyzacji?',
a: 'Standardowy montaż jednostki split w jednym pomieszczeniu zajmuje 3-5 godzin. Montaż systemu multisplit (2-4 jednostki) to zazwyczaj pełny dzień roboczy. Po zakończeniu prac sprzątamy i zostawiamy pomieszczenie w idealnym porządku.',
},
{
q: 'Czy montaż wymaga pozwolenia na budowę?',
a: 'W zdecydowanej większości przypadków montaż klimatyzatora split nie wymaga pozwolenia na budowę — wystarczy zgłoszenie. Wyjątkiem mogą być budynki zabytkowe lub objęte ochroną konserwatora. W razie wątpliwości doradzimy co zrobić.',
},
{
q: 'Trasa freonowa w bruździe czy w korytku?',
a: 'Oferujemy oba rozwiązania. Bruzda w ścianie (90 zł/mb) daje estetyczniejszy efekt — rury są niewidoczne. Korytko PCV (80 zł/mb) jest tańsze i szybsze w montażu. Doradzamy najlepsze rozwiązanie po oględzinach miejsca montażu.',
},
{
q: 'Gdzie montujecie jednostkę zewnętrzną?',
a: 'Jednostkę zewnętrzną montujemy na konsoli ściennej, dachu, balkonie lub gruncie — w zależności od warunków budynku. Zawsze dbamy o stabilność mocowania, minimalizację hałasu i estetykę instalacji.',
},
{
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ę, Dzierżoniów i okoliczne miejscowości. Koszt dojazdu ustalamy indywidualnie.',
},
];
const ETAPY = [
{ n: '01', title: 'Bezpłatna wycena', desc: 'Przyjeżdżamy lub rozmawiamy zdalnie. Oceniamy warunki montażu i doradzamy dobór urządzenia.' },
{ n: '02', title: 'Zakup urządzenia', desc: 'Możesz kupić klimatyzator u nas lub dostarczyć własny. Pracujemy z wszystkimi markami.' },
{ n: '03', title: 'Montaż', desc: 'Ekipa montażowa wykonuje instalację w uzgodnionym terminie. Sprzątamy po sobie.' },
{ n: '04', title: 'Uruchomienie i szkolenie', desc: 'Uruchamiamy urządzenie, sprawdzamy szczelność i pokazujemy jak obsługiwać klimatyzator.' },
{ n: '05', title: 'Gwarancja', desc: '2 lata gwarancji na montaż. Jesteśmy do dyspozycji w razie pytań i serwisu.' },
];
export default function MontazKlimatyzacjiPage() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Service',
name: 'Montaż klimatyzacji Wrocław',
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: 'Profesjonalny montaż klimatyzacji split, multisplit i kasetonowych na Dolnym Śląsku z uprawnieniami F-Gaz.',
offers: {
'@type': 'Offer',
priceCurrency: 'PLN',
price: '1200',
priceSpecification: { '@type': 'UnitPriceSpecification', price: '1200', priceCurrency: 'PLN', name: 'Montaż klimatyzatora split od' },
},
mainEntityOfPage: {
'@type': 'FAQPage',
mainEntity: faqItems.map(f => ({
'@type': 'Question',
name: f.q,
acceptedAnswer: { '@type': 'Answer', text: f.a },
})),
},
};
return (
<>
<JsonLd data={jsonLd} />
<main>
{/* Hero */}
<section className="relative text-white py-24 md:py-32 overflow-hidden">
<Image
src="/images/services/instalacja_klimatyzacji.webp"
alt="Montaż klimatyzacji Wrocław"
fill
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-primary/50" />
<div className="relative z-10 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" />
Montaż z uprawnieniami F-Gaz
</div>
<h1 className="text-3xl md:text-5xl font-extrabold font-heading leading-tight mb-6">
Montaż klimatyzacji Wrocław<br />
<span className="text-accent">estetycznie i z gwarancją</span>
</h1>
<p className="text-lg text-white/80 mb-8 max-w-2xl">
Montujemy klimatyzatory split, multisplit i kanałowe na terenie Wrocławia i całego Dolnego Śląska.
Uprawnienia F-Gaz, estetyczna instalacja, sprzątanie po montażu — i 2 lata gwarancji.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="md">
<Phone className="size-5" /> Zadzwoń: {companyDetails.phone}
</Button>
</a>
<Link href="/kalkulator" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Dobierz urządzenie <ArrowRight className="size-5" />
</Link>
</div>
</div>
</section>
{/* Wyróżniki */}
<section className="py-16 bg-white">
<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-8">Dlaczego warto wybrać ThermCool?</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-12">
{[
{ icon: ShieldCheck, title: 'Uprawnienia F-Gaz', desc: 'Posiadamy wymagane prawem certyfikaty F-Gaz uprawniające do pracy z czynnikami chłodniczymi R32 i R410A.' },
{ icon: Star, title: 'Estetyczna instalacja', desc: 'Ukryte korytka, schludne przewerty, brak widocznych rur — dbamy o wygląd pomieszczenia po montażu.' },
{ icon: Clock, title: 'Terminowość', desc: 'Realizujemy montaże w ustalonym terminie. Przyjeżdżamy punktualnie i kończymy w deklarowanym czasie.' },
{ icon: ShieldCheck, title: '2 lata gwarancji na montaż', desc: 'Gwarantujemy jakość wykonanej instalacji przez 2 lata. Jakikolwiek problem z montażem — naprawiamy bezpłatnie.' },
{ icon: Wrench, title: 'Wszystkie marki', desc: 'Montujemy klimatyzatory Daikin, Mitsubishi Electric, Gree, Rotenso, LG, Samsung, Haier i innych producentów.' },
{ icon: Check, title: 'Sprzątanie po montażu', desc: 'Po zakończeniu prac zbieramy gruz, kurz i opakowania. Zostawiamy pomieszczenie w takim stanie jak zastaliśmy.' },
].map((item, i) => {
const Icon = item.icon;
return (
<div key={i} className="flex gap-3 p-4 bg-slate-50 rounded-xl">
<Icon 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>
{/* Typy montażu */}
<h2 className="text-2xl font-extrabold text-primary font-heading mb-6">Rodzaje montażu</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ title: 'Klimatyzator split', desc: 'Jedna jednostka zewnętrzna + jedna wewnętrzna. Idealne do jednego pokoju lub małego mieszkania. Od 1200 zł.' },
{ title: 'System multisplit', desc: 'Jedna jednostka zewnętrzna obsługuje 2-5 pomieszczeń. Ekonomiczne rozwiązanie dla domów i biur. Wycena indywidualna.' },
{ title: 'Klimatyzator kasetonowy', desc: 'Montowany w suficie podwieszanym. Równomierne chłodzenie dużych przestrzeni biurowych i handlowych.' },
].map((t, i) => (
<div key={i} className="bg-white border border-border rounded-2xl p-5">
<h3 className="font-bold text-primary text-sm mb-2">{t.title}</h3>
<p className="text-xs text-text-muted leading-relaxed">{t.desc}</p>
</div>
))}
</div>
</div>
</section>
{/* Etapy montażu */}
<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-8">Jak wygląda montaż krok po kroku?</h2>
<div className="flex flex-col gap-4">
{ETAPY.map((e, i) => (
<div key={i} className="flex gap-4 p-5 bg-white rounded-2xl border border-border">
<div className="size-10 rounded-xl bg-accent text-white flex items-center justify-center font-extrabold text-sm shrink-0">
{e.n}
</div>
<div>
<p className="font-bold text-primary mb-1">{e.title}</p>
<p className="text-sm text-text-muted leading-relaxed">{e.desc}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* Cennik */}
<section className="py-16 bg-white">
<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-2">Cennik montażu</h2>
<p className="text-text-muted text-sm mb-8">Ceny netto. Do cen doliczany jest VAT 23%. Ostateczna wycena po oględzinach miejsca montażu.</p>
<div className="bg-slate-50 rounded-2xl border border-border overflow-hidden">
{[
{ label: 'Montaż klimatyzatora split (1 pomieszczenie)', cena: 'od 1200 zł' },
{ label: 'Montaż systemu multisplit (2 jednostki)', cena: 'od 2800 zł' },
{ label: 'Montaż systemu multisplit (3 jednostki)', cena: 'od 3800 zł' },
{ label: 'Trasa freonowa — korytko PCV', cena: '80 zł/mb' },
{ label: 'Trasa freonowa — bruzda w ścianie', cena: '90 zł/mb' },
{ label: 'Konsola ścienna jednostki zewnętrznej', cena: 'od 120 zł' },
{ label: 'Montaż kasetonowy', cena: 'wycena indywidualna' },
].map((r, i) => (
<div key={i} className={`flex items-center justify-between px-5 py-4 gap-4 ${i > 0 ? 'border-t border-border' : ''}`}>
<span className="text-sm text-primary">{r.label}</span>
<span className="text-sm font-bold text-primary whitespace-nowrap">{r.cena}</span>
</div>
))}
</div>
<div className="mt-6 text-center">
<Link href="/kontakt" className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-white font-bold rounded-xl text-sm hover:bg-accent/90 transition-colors">
Zapytaj o bezpłatną wycenę <ArrowRight className="size-4" />
</Link>
</div>
</div>
</section>
{/* FAQ */}
<section className="py-16 bg-slate-50">
<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-white 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">
Chcesz zamontować klimatyzację we Wrocławiu?
</h2>
<p className="text-white/80 mb-8">
Zadzwoń po bezpłatną wycenę — odpowiadamy szybko i przyjeżdżamy w całym Dolnym Śląsku.
</p>
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="md" className="mx-auto">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
</div>
</section>
</main>
</>
);
}
+19
View File
@@ -0,0 +1,19 @@
import Link from 'next/link';
import { buttonClasses } from '@/components/ui/Button';
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-[70vh] bg-muted px-4 text-center">
<h1 className="text-4xl md:text-5xl font-bold font-heading text-primary mb-4">
Strona nie istnieje
</h1>
<p className="text-base md:text-lg text-slate-500 mb-8 max-w-md mx-auto">
Szukana strona została usunięta lub nigdy nie istniała.
</p>
<Link href="/" className={buttonClasses({ variant: 'primary', size: 'md' })}>
Wróć na stronę główną
</Link>
</div>
);
}
+48
View File
@@ -0,0 +1,48 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { AboutStory } from '@/components/sections/AboutStory';
import { ValuesGrid } from '@/components/sections/ValuesGrid';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { companyDetails } from '@/content/company';
export const metadata: Metadata = {
title: 'O nas — ThermCool Klimatyzacja Wrocław',
description: 'Poznaj zespół specjalistów ThermCool. Certyfikat F-Gaz, profesjonalny montaż i serwis klimatyzacji oraz pomp ciepła na Dolnym Śląsku.',
alternates: {
canonical: '/o-nas',
}
};
export default function ONasPage() {
return (
<main>
<HeroSection
eyebrow="Poznaj Nas"
title={`O firmie ${companyDetails.shortName}`}
subtitle="Jesteśmy zgranym zespołem inżynierów, instalatorów i serwisantów. Od lat dostarczamy sprawdzone rozwiązania z zakresu klimatyzacji, ogrzewania i wykończeń."
ctaText="Skontaktuj się z nami"
ctaLink="/kontakt"
secondaryCtaText="Nasze realizacje"
secondaryCtaLink="/realizacje"
backgroundImage="/images/about/about-story.webp"
/>
<AboutStory />
{/* <StatsBar /> (Ukryte na czas zbierania autentycznych danych statystycznych) */}
<ValuesGrid />
{/* <CertificatesSection /> (Ukryte na czas uzyskania skanów dokumentów) */}
{/* <Testimonials /> (Ukryte na czas zbierania autentycznych opinii) */}
<CtaBanner
title="Szukasz sprawdzonej ekipy do swojego projektu?"
subtitle="Dołącz do grona naszych zadowolonych klientów. Gwarantujemy rzetelność, czystość prowadzonych prac i profesjonalne doradztwo."
ctaText="Napisz do nas"
ctaLink="/kontakt"
/>
</main>
);
}
+55
View File
@@ -0,0 +1,55 @@
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const alt = 'ThermCool - Klimatyzacja '
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
export default async function Image() {
return new ImageResponse(
(
<div
style={{
background: '#0f2a47',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'sans-serif',
}}
>
<div
style={{
fontSize: 130,
color: 'white',
fontWeight: 'bold',
marginBottom: 20,
letterSpacing: '-0.05em',
}}
>
ThermCool
</div>
<div
style={{
fontSize: 48,
color: '#2ba8e0',
fontWeight: 'normal',
letterSpacing: '-0.02em',
}}
>
Klimatyzacja — Wrocław, Dolny Śląsk
</div>
</div>
),
{
...size,
}
)
}
+299
View File
@@ -0,0 +1,299 @@
import { HeroSection } from "@/components/sections/HeroSection";
import { AdvantagesBar } from "@/components/sections/AdvantagesBar";
import { ServicesGrid } from "@/components/sections/ServicesGrid";
import { WhyUs } from "@/components/sections/WhyUs";
import { FeaturedProducts } from "@/components/sections/FeaturedProducts";
import { BlogPreview } from "@/components/sections/BlogPreview";
import { CtaBanner } from "@/components/sections/CtaBanner";
import { companyDetails } from "@/content/company";
import { Wrench, Phone, ArrowRight, Thermometer, Wind, Zap, Calendar, Check } from "lucide-react";
import Link from "next/link";
import { Button, buttonClasses } from "@/components/ui/Button";
import { JsonLd } from "@/components/ui/JsonLd";
export default function Home() {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "HVACBusiness",
"@id": `${baseUrl}/#business`,
"name": companyDetails.name,
"legalName": companyDetails.legalName,
"url": baseUrl,
"logo": {
"@type": "ImageObject",
"url": `${baseUrl}/logo.png`,
"width": 200,
"height": 60,
},
"image": `${baseUrl}/opengraph-image`,
"telephone": companyDetails.phone,
"email": companyDetails.email,
"address": {
"@type": "PostalAddress",
"streetAddress": companyDetails.address.street,
"addressLocality": companyDetails.address.city,
"postalCode": companyDetails.address.zipCode,
"addressRegion": companyDetails.address.region,
"addressCountry": "PL",
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 51.1079,
"longitude": 17.0385,
},
"priceRange": "$$",
"currenciesAccepted": "PLN",
"paymentAccepted": "Gotówka, Przelew bankowy, Karta",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "08:00",
"closes": "18:00",
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Saturday"],
"opens": "09:00",
"closes": "14:00",
},
],
"areaServed": [
{ "@type": "City", "name": "Wrocław" },
{ "@type": "City", "name": "Legnica" },
{ "@type": "City", "name": "Wałbrzych" },
{ "@type": "City", "name": "Jelenia Góra" },
{ "@type": "City", "name": "Lubin" },
{ "@type": "AdministrativeArea", "name": "Dolny Śląsk" },
],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Usługi HVAC",
"itemListElement": [
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Montaż klimatyzacji",
"description": "Profesjonalny montaż klimatyzatorów split i multisplit z uprawnieniami F-Gaz. Ceny od 1200 zł.",
},
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Montaż pomp ciepła",
"description": "Dobór i montaż pomp ciepła powietrze-woda z pomocą w uzyskaniu dofinansowania z Czystego Powietrza.",
},
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Serwis klimatyzacji",
"description": "Przeglądy, czyszczenie, odgrzybianie i naprawy klimatyzatorów. Ceny od 250 zł.",
},
},
],
},
"sameAs": [
companyDetails.socials.facebook,
companyDetails.socials.instagram,
companyDetails.socials.google,
].filter(Boolean),
},
{
"@type": "WebSite",
"@id": `${baseUrl}/#website`,
"url": baseUrl,
"name": "ThermCool",
"description": "Klimatyzacja na Dolnym Śląsku",
"inLanguage": "pl-PL",
"publisher": { "@id": `${baseUrl}/#business` },
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": `${baseUrl}/sklep?search={search_term_string}`,
},
"query-input": "required name=search_term_string",
},
},
],
};
return (
<>
{/* LocalBusiness Schema for Google Search SEO */}
<JsonLd data={jsonLd} />
<HeroSection />
<AdvantagesBar />
<ServicesGrid />
{/* BrandsBar will go here in Group B */}
<FeaturedProducts />
<WhyUs />
{/* ProjectsSection will go here in Group B */}
{/* <Testimonials /> (Ukryte na czas zbierania autentycznych opinii) */}
<BlogPreview />
{/* Cennik usług */}
{/* Cennik usług */}
<section className="bg-white py-16 px-4">
<div className="bg-slate-50 max-w-7xl mx-auto rounded-[2.5rem] p-6 sm:p-10 lg:p-14">
<div className="max-w-7xl mx-auto flex flex-col gap-8">
{/* Header (Sekcja hero nad kartami) */}
<div className="mb-2">
<div className="inline-flex items-center gap-1.5 text-[10px] font-bold text-accent uppercase tracking-widest mb-6 bg-accent/5 border border-accent/20 px-3 py-1.5 rounded-full">
<Check className="size-3.5" />
Cennik usług
</div>
<h2 className="text-3xl sm:text-5xl font-extrabold text-primary font-heading leading-tight mb-4">
Montaż, serwis i czyszczenie klimatyzacji –<br />już od <span className="underline decoration-accent underline-offset-4">150 zł</span>
</h2>
<p className="text-text-muted text-sm sm:text-base max-w-2xl leading-relaxed">
Profesjonalna obsługa, certyfikowani specjaliści i szybki czas reakcji na terenie całego Wrocławia. Zadbaj o czyste powietrze i wydajność swoich urządzeń.
</p>
</div>
{/* Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Card 1: Montaż klimatyzacji */}
<div className="bg-white p-8 flex flex-col rounded-2xl shadow-sm border border-border">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-100 flex items-center justify-center shrink-0 border border-slate-200 rounded-lg">
<Wrench className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">Montaż klimatyzacji</h3>
<p className="text-sm text-text-muted leading-relaxed">Profesjonalny montaż urządzeń split i multisplit. Zapewniamy estetykę wykonania i pełne uruchomienie.</p>
</div>
</div>
<div className="border-t border-border pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-1">Cena usługi</p>
<p className="text-xl font-extrabold text-primary">od 1400 zł</p>
<p className="text-[10px] text-text-muted mt-1">* cena standardowego montażu do 5m</p>
</div>
</div>
{/* Card 2: Czyszczenie */}
<div className="bg-white p-8 flex flex-col rounded-2xl shadow-sm border border-border">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-100 flex items-center justify-center shrink-0 border border-slate-200 rounded-lg">
<Thermometer className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">Czyszczenie i odgrzybianie</h3>
<p className="text-sm text-text-muted leading-relaxed">Kompleksowe czyszczenie, dezynfekcja chemiczna, odgrzybianie parowe, czyszczenie filtrów.</p>
</div>
</div>
<div className="border-t border-border pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-1">Cena usługi</p>
<p className="text-xl font-extrabold text-primary">od 250 zł</p>
</div>
</div>
{/* Card 3: Przegląd techniczny */}
<div className="bg-white p-8 flex flex-col rounded-2xl shadow-sm border border-border">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-100 flex items-center justify-center shrink-0 border border-slate-200 rounded-lg">
<Wind className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">Przegląd techniczny</h3>
<p className="text-sm text-text-muted leading-relaxed">Sprawdzenie parametrów pracy, kontrola szczelności układu, pomiar ciśnień roboczych.</p>
</div>
</div>
<div className="border-t border-border pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-1">Cena usługi</p>
<p className="text-xl font-extrabold text-primary">od 150 zł</p>
</div>
</div>
{/* Card 4: Uzupełnienie czynnika */}
<div className="bg-white p-8 flex flex-col rounded-2xl shadow-sm border border-border">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-100 flex items-center justify-center shrink-0 border border-slate-200 rounded-lg">
<Zap className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">Uzupełnienie czynnika</h3>
<p className="text-sm text-text-muted leading-relaxed">Lokalizacja nieszczelności, próba ciśnieniowa azotem, dobicie czynnika chłodniczego.</p>
</div>
</div>
<div className="border-t border-border pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-1">Cena usługi</p>
<p className="text-xl font-extrabold text-primary">od 180 zł</p>
</div>
</div>
{/* Card 5: Naprawa usterek */}
<div className="bg-white p-8 flex flex-col rounded-2xl shadow-sm border border-border">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-100 flex items-center justify-center shrink-0 border border-slate-200 rounded-lg">
<Wrench className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">Naprawa usterek</h3>
<p className="text-sm text-text-muted leading-relaxed">Diagnoza awarii, wymiana uszkodzonych podzespołów elektroniki, sprężarek, wentylatorów.</p>
</div>
</div>
<div className="border-t border-border pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-1">Cena usługi</p>
<p className="text-sm font-extrabold text-primary">Wycena indywidualna</p>
</div>
</div>
{/* Card 6: Serwis sezonowy (Dark card) */}
<div className="bg-[#1a2332] p-8 flex flex-col border border-[#1a2332] shadow-sm rounded-2xl">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-800 flex items-center justify-center shrink-0 border border-slate-700 rounded-lg">
<Calendar className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-white font-heading mb-3">Serwis sezonowy / Abonament</h3>
<p className="text-sm text-white/60 leading-relaxed">Regularna opieka nad instalacją przed sezonem letnim i zimowym. Priorytetowy czas reakcji w przypadku awarii.</p>
</div>
</div>
<div className="border-t border-white/10 pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-white/60 mb-1">Cena usługi</p>
<p className="text-xl font-extrabold text-white">od 199 zł / rok</p>
</div>
</div>
{/* Card 6: Gotowy na świeże powietrze? (Full width) */}
<div className="bg-white md:col-span-3 p-8 md:p-10 flex flex-col lg:flex-row items-start lg:items-center justify-between gap-8 border-l-[6px] border-accent rounded-2xl shadow-sm">
<div className="max-w-2xl">
<h3 className="text-2xl font-bold text-primary font-heading mb-3">Gotowy na świeże powietrze?</h3>
<p className="text-sm text-text-muted leading-relaxed">Nasi certyfikowani technicy są dostępni od poniedziałku do soboty. Zarezerwuj termin już dziś, aby uniknąć kolejek w sezonie.</p>
</div>
<div className="flex flex-col sm:flex-row items-center gap-4 shrink-0 w-full lg:w-auto justify-center lg:justify-end">
<Link href="/kontakt" className="w-fit sm:w-auto flex justify-center items-center gap-2 px-6 py-3.5 bg-accent text-white font-bold text-sm hover:bg-accent/90 transition-colors rounded-xl">
Umów serwis <Calendar className="size-4" />
</Link>
<a href={`tel:${companyDetails.phoneRaw}`} className="w-fit sm:w-auto">
<button className="w-fit sm:w-auto flex justify-center items-center gap-2 px-6 py-3.5 bg-white text-primary border border-border font-bold text-sm hover:bg-slate-50 transition-colors rounded-xl">
<Phone className="size-4" /> Zadzwoń do nas
</button>
</a>
</div>
</div>
</div>
</div>
</div>
</section>
<CtaBanner bgVariant="accent-light" />
</>
);
}
+106
View File
@@ -0,0 +1,106 @@
'use client';
import { useCompareStore } from '@/lib/compare-store';
import Image from 'next/image';
import Link from 'next/link';
import { X, ShoppingCart } from 'lucide-react';
import { useCartStore } from '@/lib/cart-store';
import { buttonClasses } from '@/components/ui/Button';
export default function PorownajPage() {
const { items, removeItem } = useCompareStore();
const addToCart = useCartStore((s) => s.addItem);
if (items.length === 0) {
return (
<main className="min-h-screen bg-slate-50 flex items-center justify-center">
<div className="text-center">
<p className="text-text-muted mb-4">Nie masz żadnych produktów do porównania.</p>
<Link href="/sklep" className={buttonClasses({ variant: 'primary', size: 'md' })}>
Wróć do sklepu
</Link>
</div>
</main>
);
}
const allSpecKeys = [...new Set(items.flatMap((i) => Object.keys(i.specs)))];
return (
<main className="bg-slate-50 min-h-screen py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between mb-8">
<h1 className="text-2xl font-bold text-primary font-heading">Porównanie produktów</h1>
<Link href="/sklep" className="text-sm text-accent hover:underline">← Wróć do sklepu</Link>
</div>
<div className="overflow-x-auto">
<table className="w-full bg-white rounded-xl border border-border overflow-hidden">
<thead>
<tr className="border-b border-border">
<td className="p-4 w-36 bg-slate-50" />
{items.map((item) => (
<td key={item.slug} className="p-4 text-center align-top border-l border-border">
<div className="relative mx-auto size-24 mb-3">
<Image
src={item.image}
alt={item.name}
fill
className="object-contain"
unoptimized={item.image.startsWith('http')}
/>
</div>
<p className="text-xs text-accent font-semibold uppercase mb-1">{item.brand}</p>
<p className="text-sm font-bold text-primary font-heading leading-snug">{item.name}</p>
{item.price && (
<p className="text-lg font-bold text-primary mt-2">
{item.price.toLocaleString('pl-PL')} zł
</p>
)}
<div className="flex gap-2 mt-3 justify-center">
{item.price && (
<button
onClick={() => addToCart({
slug: item.slug,
name: item.name,
brand: item.brand,
image: item.image,
price: item.price!,
})}
className="flex items-center gap-1 px-3 py-1.5 bg-accent text-white rounded-lg text-xs font-bold hover:bg-accent/90 transition-colors"
>
<ShoppingCart className="size-3" /> Do koszyka
</button>
)}
<button
onClick={() => removeItem(item.slug)}
className="p-1.5 text-text-muted hover:text-primary border border-border rounded-lg transition-colors"
>
<X className="size-3.5" />
</button>
</div>
</td>
))}
</tr>
</thead>
<tbody>
{allSpecKeys.map((key, idx) => (
<tr key={key} className={`border-b border-border ${idx % 2 === 0 ? 'bg-white' : 'bg-slate-50'}`}>
<td className="p-3 text-xs font-medium text-text-muted bg-slate-50 border-r border-border">
{key}
</td>
{items.map((item) => (
<td key={item.slug} className="p-3 text-sm text-primary text-center border-l border-border">
{item.specs[key] || <span className="text-border">—</span>}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</main>
);
}
+45
View File
@@ -0,0 +1,45 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { ProjectsGrid } from '@/components/sections/ProjectsGrid';
import { StatsBar } from '@/components/sections/StatsBar';
import { CtaBanner } from '@/components/sections/CtaBanner';
export const metadata: Metadata = {
title: 'Realizacje',
description: 'Zobacz nasze wybrane realizacje z zakresu klimatyzacji, pomp ciepła, remontów i usług hydraulicznych.',
alternates: {
canonical: '/realizacje',
},
robots: {
index: false,
follow: false,
}
};
export default function RealizacjePage() {
return (
<main>
<HeroSection
eyebrow="Portfolio"
title="Nasze realizacje"
subtitle="Zobacz wybrane projekty, które zrealizowaliśmy dla naszych klientów. Każde zlecenie traktujemy indywidualnie, dbając o najwyższą jakość wykonania."
ctaText="Zobacz ofertę"
ctaLink="/uslugi-domowe"
secondaryCtaText="Skontaktuj się"
secondaryCtaLink="/kontakt"
backgroundImage="/images/hero/hero-section-image.webp"
/>
<ProjectsGrid />
<StatsBar />
<CtaBanner
title="Chcesz uzyskać podobny efekt?"
subtitle="Umów się na bezpłatną konsultację. Doradzimy, przygotujemy projekt i wycenimy prace bez żadnych zobowiązań."
ctaText="Zamów darmową wycenę"
ctaLink="/kontakt"
/>
</main>
);
}
+12
View File
@@ -0,0 +1,12 @@
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
// Możemy tu dodać disallow np. do panelu admina, jeśli by powstał w przyszłości
},
sitemap: 'https://thermcool.pl/sitemap.xml',
};
}
+272
View File
@@ -0,0 +1,272 @@
import type { Metadata } from 'next';
import { JsonLd } from '@/components/ui/JsonLd';
import Link from 'next/link';
import Image from 'next/image';
import { Check, Phone, Wrench, ArrowRight, Thermometer, Wind, Zap, ShieldCheck } from 'lucide-react';
import { companyDetails } from '@/content/company';
import { buttonClasses, Button } from '@/components/ui/Button';
export const metadata: Metadata = {
title: 'Serwis klimatyzacji Wrocław — przegląd, czyszczenie, naprawa',
description: 'Serwis klimatyzacji we Wrocławiu i na Dolnym Śląsku. Czyszczenie, odgrzybianie, przegląd, naprawa — wszystkie marki. Szybki termin, uczciwa cena. Zadzwoń!',
alternates: { canonical: '/serwis-klimatyzacji' },
openGraph: {
title: 'Serwis klimatyzacji Wrocław',
description: 'Czyszczenie, przegląd i naprawa klimatyzatorów wszystkich marek. Wrocław i Dolny Śląsk.',
},
};
const faqItems = [
{
q: 'Jak często należy serwisować klimatyzację?',
a: 'Zalecamy przegląd i czyszczenie klimatyzatora co najmniej raz w roku — najlepiej przed sezonem letnim (kwiecień–maj). Urządzenia pracujące intensywnie lub w zapylonych pomieszczeniach warto serwisować dwa razy do roku. Regularne przeglądy wydłużają żywotność urządzenia i obniżają rachunki za prąd.',
},
{
q: 'Ile kosztuje czyszczenie klimatyzatora?',
a: 'Czyszczenie i odgrzybianie klimatyzatora split zaczyna się od 250 zł. Cena obejmuje czyszczenie filtrów, lameli, tacy ociekowej, wentylatora i dezynfekcję układu nawiewu. Dokładna cena zależy od stanu urządzenia i jego modelu.',
},
{
q: 'Jakie marki klimatyzatorów serwisujecie?',
a: 'Serwisujemy wszystkie popularne marki: Daikin, Mitsubishi Electric, LG, Samsung, Gree, Rotenso, Haier, Aux, Fuji Electric, Toshiba, Fujitsu i inne. Jeśli masz mniej popularny model — zadzwoń, prawdopodobnie też go obsługujemy.',
},
{
q: 'Czy przyjeżdżacie w ten sam dzień?',
a: 'Staramy się zapewnić wizytę serwisową w ciągu 24–48 godzin od zgłoszenia. W przypadku awarii krytycznych (brak chłodzenia podczas upałów) priorytetyzujemy pilne wyjazdy. Zadzwoń i opisz sytuację — dopasujemy termin.',
},
{
q: 'Co obejmuje przegląd techniczny klimatyzatora?',
a: 'Przegląd obejmuje: kontrolę szczelności układu chłodniczego (wymóg F-Gaz), pomiar ciśnień roboczych i temperatur, sprawdzenie układu elektrycznego, kontrolę wentylatora i silnika, czyszczenie filtrów oraz raport z przeglądu.',
},
{
q: 'Czy serwisujecie klimatyzatory na gwarancji?',
a: 'Tak, wykonujemy przeglądy gwarancyjne które są wymagane przez producentów do zachowania gwarancji. Wystawiamy dokumentację potwierdzającą wykonanie przeglądu. W przypadku usterek gwarancyjnych kontaktujemy się z importerem w imieniu klienta.',
},
];
const USLUGI = [
{
icon: Thermometer,
title: 'Czyszczenie i odgrzybianie',
desc: 'Kompleksowe czyszczenie jednostki wewnętrznej i zewnętrznej. Dezynfekcja chemiczna, odgrzybianie parowe, czyszczenie filtrów i tacy ociekowej.',
cena: 'od 250 zł',
},
{
icon: Wind,
title: 'Przegląd techniczny (F-Gaz)',
desc: 'Kontrola szczelności układu chłodniczego, pomiar ciśnień i temperatur, sprawdzenie układu elektrycznego. Wymagany prawem co 1–2 lata.',
cena: 'od 150 zł',
},
{
icon: Zap,
title: 'Uzupełnienie czynnika chłodniczego',
desc: 'Lokalizacja nieszczelności próbą azotem, próżniowanie układu, dobicie czynnika R32 lub R410A do właściwego ciśnienia.',
cena: 'od 180 zł',
},
{
icon: Wrench,
title: 'Naprawa i diagnostyka',
desc: 'Odczyt kodów błędów, wymiana uszkodzonych podzespołów elektroniki, sprężarki, wentylatora, czujników. Oryginalne części zamienne.',
cena: 'wycena po diagnostyce',
},
{
icon: ShieldCheck,
title: 'Serwis gwarancyjny',
desc: 'Przeglądy gwarancyjne wymagane przez producenta do zachowania gwarancji. Pełna dokumentacja i raport z przeglądu.',
cena: 'od 150 zł',
},
{
icon: Check,
title: 'Serwis sezonowy / abonament',
desc: '2 przeglądy rocznie (przed latem i zimą), priorytetowy czas reakcji i 15% zniżki na naprawy. Idealne dla biur i restauracji.',
cena: 'od 199 zł/rok',
},
];
const BRANDS_WITH_LOGOS = [
{ name: 'Aux', src: '/brands/aux-brand.webp' },
{ name: 'CHV', src: '/brands/chv.webp' },
{ name: 'Fuji Electric', src: '/brands/fuji.webp' },
{ name: 'Gree', src: '/brands/gree.webp' },
{ name: 'Haier', src: '/brands/haier.webp' },
{ name: 'LG', src: '/brands/lg.webp' },
{ name: 'Mitsubishi Electric', src: '/brands/mitsubishi.webp' },
{ name: 'Rotenso', src: '/brands/rotenso.webp' },
{ name: 'Samsung', src: '/brands/samsung.webp' },
{ name: 'Toshiba', src: '/brands/toshiba.webp' },
];
export default function SerwisKlimatyzacjiPage() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Service',
name: 'Serwis klimatyzacji Wrocław',
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, czyszczenie, przegląd i naprawa klimatyzatorów wszystkich marek na Dolnym Śląsku.',
offers: {
'@type': 'Offer',
priceCurrency: 'PLN',
price: '250',
priceSpecification: { '@type': 'UnitPriceSpecification', price: '250', priceCurrency: 'PLN', name: 'Czyszczenie klimatyzatora od' },
},
mainEntityOfPage: {
'@type': 'FAQPage',
mainEntity: faqItems.map(f => ({
'@type': 'Question',
name: f.q,
acceptedAnswer: { '@type': 'Answer', text: f.a },
})),
},
};
return (
<>
<JsonLd data={jsonLd} />
<main>
{/* Hero */}
<section className="relative text-white py-24 md:py-32 overflow-hidden">
<Image
src="/images/services/czyszczenie.webp"
alt="Serwis klimatyzacji Wrocław"
fill
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-primary/50" />
<div className="relative z-10 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" />
Wszystkie marki — Dolny Śląsk
</div>
<h1 className="text-3xl md:text-5xl font-extrabold font-heading leading-tight mb-6">
Serwis klimatyzacji Wrocław<br />
<span className="text-accent">szybko i z gwarancją jakości</span>
</h1>
<p className="text-lg text-white/80 mb-8 max-w-2xl">
Czyszczenie, odgrzybianie, przeglądy techniczne i naprawy klimatyzatorów wszystkich marek.
Działamy na terenie Wrocławia i całego Dolnego Śląska — szybki termin, uczciwa cena.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="md">
<Phone className="size-5" /> Zadzwoń: {companyDetails.phone}
</Button>
</a>
<Link href="/zgloszenie-awarii" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Zgłoś awarię online <ArrowRight className="size-5" />
</Link>
</div>
</div>
</section>
{/* Usługi serwisowe */}
<section className="py-16 bg-white">
<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-8">Co obejmuje nasz serwis?</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{USLUGI.map((u, i) => {
const Icon = u.icon;
return (
<div key={i} className="flex gap-4 p-5 bg-slate-50 rounded-2xl border border-border">
<div className="size-10 rounded-lg bg-accent/10 flex items-center justify-center shrink-0">
<Icon className="size-5 text-accent" />
</div>
<div className="flex-1">
<div className="flex items-start justify-between gap-2 mb-1">
<p className="font-bold text-primary text-sm">{u.title}</p>
<span className="text-xs font-bold text-accent whitespace-nowrap">{u.cena}</span>
</div>
<p className="text-xs text-text-muted leading-relaxed">{u.desc}</p>
</div>
</div>
);
})}
</div>
</div>
</section>
{/* Marki */}
<section className="py-16 bg-slate-50 overflow-hidden">
<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">Serwisujemy wszystkie popularne marki</h2>
<div className="relative flex overflow-hidden w-full mb-4">
<div className="flex w-max animate-marquee gap-4 whitespace-nowrap items-center">
{/* Pierwszy zestaw */}
{BRANDS_WITH_LOGOS.map(m => (
<div key={m.name} className="flex-shrink-0 px-6 py-4 bg-white border border-border rounded-xl flex items-center justify-center w-40 h-20 hover:shadow-md transition-shadow">
<Image src={m.src} alt={m.name} width={120} height={60} className="object-contain w-full h-full" />
</div>
))}
{/* Drugi zestaw (klon) do płynnego przejścia */}
{BRANDS_WITH_LOGOS.map(m => (
<div key={`dup-${m.name}`} className="flex-shrink-0 px-6 py-4 bg-white border border-border rounded-xl flex items-center justify-center w-40 h-20 hover:shadow-md transition-shadow">
<Image src={m.src} alt={m.name} width={120} height={60} className="object-contain w-full h-full" />
</div>
))}
</div>
</div>
<p className="text-xs text-text-muted">Nie widzisz swojej marki? Zadzwoń — prawdopodobnie też ją 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">
Twój klimatyzator wymaga serwisu?
</h2>
<p className="text-white/80 mb-8">
Zadzwoń lub zgłoś awarię online — przyjeżdżamy na terenie Wrocławia i Dolnego Śląska.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href={`tel:${companyDetails.phoneRaw}`}>
<Button variant="primary" size="md">
<Phone className="size-5" /> {companyDetails.phone}
</Button>
</a>
<Link href="/zgloszenie-awarii" className={buttonClasses({ variant: 'outline', size: 'md' })}>
Zgłoś online <ArrowRight className="size-5" />
</Link>
</div>
</div>
</section>
</main>
</>
);
}
+58
View File
@@ -0,0 +1,58 @@
import { MetadataRoute } from 'next';
import { getProducts } from '@/content/products';
import { getPosts } from '@/content/posts';
import { dolnySlaskCities } from '@/content/cities';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://thermcool.pl';
// Statyczne trasy (Główne)
const staticRoutes = [
'',
'/klimatyzacja',
'/blog',
'/o-nas',
'/kalkulator',
'/kontakt',
'/sklep',
'/koszyk',
'/porownaj',
'/regulamin',
'/polityka-prywatnosci',
'/reklamacje',
'/montaz-klimatyzacji',
'/serwis-klimatyzacji',
'/cennik-montazu',
'/dostawa',
'/gwarancja',
].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date().toISOString(),
changeFrequency: 'weekly' as const,
priority: route === '' ? 1 : 0.8,
}));
const products = getProducts();
const productRoutes = products.map((product) => ({
url: `${baseUrl}/sklep/${product.slug}`,
lastModified: new Date().toISOString(),
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
const posts = getPosts();
const postRoutes = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: post.date,
changeFrequency: 'monthly' as const,
priority: 0.6,
}));
const cityRoutes = dolnySlaskCities.map((city) => ({
url: `${baseUrl}/klimatyzacja/${city.slug}`,
lastModified: new Date().toISOString(),
changeFrequency: 'monthly' as const,
priority: 0.7,
}));
return [...staticRoutes, ...productRoutes, ...postRoutes, ...cityRoutes];
}
+70
View File
@@ -0,0 +1,70 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getProductBySlug, getProducts } from '@/content/products';
import { companyDetails } from '@/content/company';
import { ProductPageClient } from '@/components/shop/ProductPageClient';
import { getProductVariants } from '@/content/product-variants';
import { JsonLd } from '@/components/ui/JsonLd';
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
return getProducts().map(p => ({ slug: p.slug }));
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const product = getProductBySlug(slug);
if (!product) return { title: 'Produkt nie znaleziony' };
return {
title: `${product.brand} ${product.name}`,
description: product.shortDescription || `Klimatyzator ${product.name} — sprawdź specyfikację i cenę. Profesjonalny montaż na Dolnym Śląsku.`,
alternates: { canonical: `/sklep/${product.slug}` },
};
}
export default async function ProductPage({ params }: Props) {
const { slug } = await params;
const product = getProductBySlug(slug);
if (!product) notFound();
// Pobierz wszystkie produkty serii dla wariantów
const variants = getProductVariants(slug);
const allProducts = getProducts();
const seriesProducts = variants
? allProducts.filter(p => getProductVariants(p.slug)?.series === variants.series)
: [product];
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
const productJsonLd = product.price ? {
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
"description": product.shortDescription,
"sku": product.sku,
"brand": { "@type": "Brand", "name": product.brand },
"image": product.image.startsWith('http') ? product.image : `${baseUrl}${product.image}`,
"offers": {
"@type": "Offer",
"url": `${baseUrl}/sklep/${product.slug}`,
"priceCurrency": "PLN",
"price": product.price,
"seller": { "@type": "Organization", "name": "ThermCool" },
},
} : null;
return (
<>
{productJsonLd && <JsonLd data={productJsonLd} />}
<ProductPageClient
product={product}
seriesProducts={seriesProducts}
companyPhone={companyDetails.phone}
companyPhoneRaw={companyDetails.phoneRaw}
/>
</>
);
}
+33
View File
@@ -0,0 +1,33 @@
import { Metadata } from 'next';
import { getProducts, getBrands } from '@/content/products';
import { ProductsGrid } from '@/components/shop/ProductsGrid';
import { HeroSection } from '@/components/sections/HeroSection';
export const metadata: Metadata = {
title: 'Sklep z klimatyzatorami',
description: 'Klimatyzatory split i multisplit — Gree, Rotenso, Haier, Daikin, LG, Samsung i inne. Szeroki wybór, konkurencyjne ceny, profesjonalny montaż.',
alternates: { canonical: '/sklep' },
};
export default function ShopPage() {
const products = getProducts();
const brands = getBrands();
const splitCount = products.filter(p => p.category === 'scienne').length;
const multiCount = products.filter(p => p.category === 'multisplit').length;
return (
<main>
<HeroSection
eyebrow="Sklep ThermCool"
title="Klimatyzatory split i multisplit"
subtitle={`${splitCount} klimatyzatorów split i ${multiCount} jednostek multisplit. Gree, Rotenso, Haier, Daikin, LG, Samsung i inne marki. Z montażem lub bez.`}
ctaText="Zapytaj o montaż"
ctaLink="/kontakt"
secondaryCtaText=""
secondaryCtaLink=""
backgroundImage="/images/hero/shop-section-image.webp"
/>
<ProductsGrid products={products} brands={brands} />
</main>
);
}
+55
View File
@@ -0,0 +1,55 @@
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const alt = 'ThermCool - Klimatyzacja '
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
export default async function Image() {
return new ImageResponse(
(
<div
style={{
background: '#0f2a47',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'sans-serif',
}}
>
<div
style={{
fontSize: 130,
color: 'white',
fontWeight: 'bold',
marginBottom: 20,
letterSpacing: '-0.05em',
}}
>
ThermCool
</div>
<div
style={{
fontSize: 48,
color: '#2ba8e0',
fontWeight: 'normal',
letterSpacing: '-0.02em',
}}
>
Klimatyzacja — Wrocław, Dolny Śląsk
</div>
</div>
),
{
...size,
}
)
}
+135
View File
@@ -0,0 +1,135 @@
'use server';
import { z } from 'zod';
import { headers } from 'next/headers';
import { getProductBySlug } from '@/content/products';
import { escapeHtml, rateLimit, verifyRecaptcha, baseContactSchema } from '@/lib/security';
import { getMailClient } from '@/lib/mail';
const orderSchema = baseContactSchema.extend({
address: z.string().optional(),
notes: z.string().optional(),
products: z.string(),
total: z.string(), // We don't trust this total, just checking if it exists
});
const productItemSchema = z.array(z.object({
slug: z.string(),
quantity: z.number().int().min(1).max(100),
}));
export async function submitOrderForm(prevState: unknown, formData: FormData) {
try {
const ip = (await headers()).get('x-forwarded-for') || 'unknown';
if (!rateLimit(ip)) {
return { success: false, message: 'Wysłano zbyt wiele zapytań. Spróbuj ponownie później.' };
}
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && process.env.RECAPTCHA_SECRET_KEY) {
const token = formData.get('recaptchaToken') as string;
if (!token) {
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
}
const success = await verifyRecaptcha(token);
if (!success) {
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
}
}
const mailClient = getMailClient();
if (!mailClient) {
return { success: false, message: "Funkcja wysyłania wiadomości jest tymczasowo niedostępna." };
}
const data = orderSchema.parse({
name: formData.get('name'),
phone: formData.get('phone'),
email: formData.get('email'),
address: formData.get('address') || '',
notes: formData.get('notes') || '',
consent: formData.get('consent'),
products: formData.get('products'),
total: formData.get('total'),
});
let rawProducts;
try {
rawProducts = JSON.parse(data.products);
} catch {
return { success: false, message: "Błąd przetwarzania koszyka." };
}
const parsedProducts = productItemSchema.parse(rawProducts);
if (parsedProducts.length === 0) {
return { success: false, message: "Koszyk jest pusty." };
}
let calculatedTotal = 0;
const validatedProducts = [];
for (const p of parsedProducts) {
const dbProduct = getProductBySlug(p.slug);
if (!dbProduct || !dbProduct.price) {
return { success: false, message: "Jeden z produktów w koszyku jest niedostępny." };
}
validatedProducts.push({
name: dbProduct.name,
quantity: p.quantity,
price: dbProduct.price
});
calculatedTotal += dbProduct.price * p.quantity;
}
const productRows = validatedProducts.map(p =>
`<tr><td style="padding:8px;border-bottom:1px solid #f1f5f9;">${escapeHtml(p.name)}</td><td style="padding:8px;border-bottom:1px solid #f1f5f9;text-align:center;">${p.quantity}</td><td style="padding:8px;border-bottom:1px solid #f1f5f9;text-align:right;">${(p.price * p.quantity).toLocaleString('pl-PL')} zł</td></tr>`
).join('');
const { data: emailData, error } = await mailClient.resend.emails.send({
from: mailClient.fromEmail,
to: mailClient.toEmail,
replyTo: data.email,
subject: `Nowe zamówienie od ${escapeHtml(data.name)} — ${calculatedTotal.toLocaleString('pl-PL')} zł`,
html: `
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;">
<div style="background:#0F2A47;padding:20px;text-align:center;">
<h1 style="color:#fff;margin:0;font-size:20px;">Nowe zamówienie — ThermCool</h1>
</div>
<div style="padding:24px;">
<h2 style="font-size:16px;margin-top:0;">Dane klienta</h2>
<p><strong>Imię:</strong> ${escapeHtml(data.name)}</p>
<p><strong>Telefon:</strong> <a href="tel:${escapeHtml(data.phone)}">${escapeHtml(data.phone)}</a></p>
<p><strong>Email:</strong> ${escapeHtml(data.email)}</p>
${data.address ? `<p><strong>Adres:</strong> ${escapeHtml(data.address)}</p>` : ''}
${data.notes ? `<p><strong>Uwagi:</strong> ${escapeHtml(data.notes)}</p>` : ''}
<h2 style="font-size:16px;">Zamówione produkty</h2>
<table style="width:100%;border-collapse:collapse;">
<thead><tr style="background:#f8fafc;">
<th style="padding:8px;text-align:left;">Produkt</th>
<th style="padding:8px;text-align:center;">Ilość</th>
<th style="padding:8px;text-align:right;">Wartość</th>
</tr></thead>
<tbody>${productRows}</tbody>
<tfoot><tr>
<td colspan="2" style="padding:8px;font-weight:bold;">Łącznie (urządzenia)</td>
<td style="padding:8px;font-weight:bold;text-align:right;">${calculatedTotal.toLocaleString('pl-PL')} zł</td>
</tr></tfoot>
</table>
<p style="margin-top:16px;font-size:12px;color:#64748b;">+ koszt montażu do ustalenia telefonicznie</p>
</div>
</div>
`,
});
if (error) {
console.error("Resend Error:", error);
return { success: false, message: "Nie udało się wysłać wiadomości ze względu na błąd serwera pocztowego." };
}
console.log("Wysłano e-mail (ID):", emailData?.id);
return { success: true, message: 'Dziękujemy! Skontaktujemy się z Tobą w ciągu 24 godzin, aby potwierdzić zamówienie i ustalić termin montażu.' };
} catch (error) {
console.error("Błąd formularza:", error);
return { success: false, message: 'Wystąpił błąd podczas wysyłania zamówienia. Spróbuj ponownie.' };
}
}
+17
View File
@@ -0,0 +1,17 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Zamówienie',
robots: {
index: false,
follow: true,
},
};
export default function ZamowienieLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}
+145
View File
@@ -0,0 +1,145 @@
'use client';
import { useCartStore } from '@/lib/cart-store';
import { useActionState, useEffect, startTransition } from 'react';
import Link from 'next/link';
import { submitOrderForm } from './actions';
import { ArrowLeft, CheckCircle2 } from 'lucide-react';
import { buttonClasses } from '@/components/ui/Button';
import { getRecaptchaToken } from '@/lib/getRecaptchaToken';
const initialState = { success: false, message: '' };
export default function ZamowieniePage() {
const { items, getTotal, clearCart } = useCartStore();
const [state, formAction, pending] = useActionState(submitOrderForm, initialState);
const total = getTotal();
useEffect(() => {
if (state.success) clearCart();
}, [state.success, clearCart]);
if (items.length === 0 && !state.success) {
return (
<main className="min-h-screen bg-slate-50 flex items-center justify-center">
<div className="text-center">
<p className="text-text-muted mb-4">Koszyk jest pusty.</p>
<Link href="/sklep" className={buttonClasses({ variant: 'primary', size: 'md' })}>
Wróć do sklepu
</Link>
</div>
</main>
);
}
if (state.success) {
return (
<main className="min-h-screen bg-slate-50 flex items-center justify-center py-16">
<div className="max-w-md mx-auto px-4 w-full">
<div className="bg-green-50 text-green-800 p-8 rounded-2xl border border-green-200 flex flex-col items-center text-center">
<CheckCircle2 className="w-16 h-16 text-green-500 mb-4" />
<h3 className="text-2xl font-bold font-heading mb-2">Zamówienie przyjęte!</h3>
<p className="text-green-700">{state.message}</p>
<Link href="/sklep" className="mt-6 px-6 py-2 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700 transition-colors">
Wróć do sklepu
</Link>
</div>
</div>
</main>
);
}
return (
<main className="bg-slate-50 min-h-screen py-8">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<Link href="/koszyk" className="inline-flex items-center gap-2 text-sm text-text-muted hover:text-accent mb-6">
<ArrowLeft className="size-4" /> Wróć do koszyka
</Link>
<h1 className="text-2xl font-bold text-primary font-heading mb-8">Złóż zamówienie</h1>
<div className="grid grid-cols-1 gap-6">
{/* Podsumowanie produktów */}
<div className="bg-white rounded-xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4">Zamawiane produkty</h2>
<div className="flex flex-col gap-2">
{items.map((item) => (
<div key={item.id} className="flex justify-between text-sm">
<span className="text-text-muted">{item.name} × {item.quantity}</span>
<span className="font-semibold text-primary">{(item.price * item.quantity).toLocaleString('pl-PL')} zł</span>
</div>
))}
<div className="border-t border-border pt-2 mt-2 flex justify-between font-bold text-primary">
<span>Łącznie (urządzenia)</span>
<span>{total.toLocaleString('pl-PL')} zł</span>
</div>
</div>
</div>
{/* Formularz */}
<form onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const token = await getRecaptchaToken('submit');
if (token) {
formData.append('recaptchaToken', token);
}
startTransition(() => {
formAction(formData);
});
}} className="bg-white rounded-xl border border-border p-6 flex flex-col gap-4">
<h2 className="text-base font-bold text-primary font-heading">Twoje dane</h2>
{/* Hidden: produkty jako JSON */}
<input type="hidden" name="products" value={JSON.stringify(items)} />
<input type="hidden" name="total" value={total} />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Imię i nazwisko *</label>
<input name="name" required className="w-full border border-border rounded-lg 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 name="phone" required type="tel" className="w-full border border-border rounded-lg 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">Email *</label>
<input name="email" required type="email" className="w-full border border-border rounded-lg px-3 py-2.5 text-sm focus:border-accent outline-none" placeholder="[email protected]" />
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Adres dostawy / montażu</label>
<input name="address" className="w-full border border-border rounded-lg px-3 py-2.5 text-sm focus:border-accent outline-none" placeholder="ul. Przykładowa 1, 50-000 Wrocław" />
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Uwagi do zamówienia</label>
<textarea name="notes" rows={3} className="w-full border border-border rounded-lg px-3 py-2.5 text-sm focus:border-accent outline-none resize-none" placeholder="Np. preferowany termin montażu, piętro, specjalne wymagania..." />
</div>
<label className="flex items-start gap-3 cursor-pointer">
<input type="checkbox" name="consent" required className="mt-0.5 w-5 h-5 accent-accent shrink-0 cursor-pointer" />
<span className="text-xs text-text-muted">Wyrażam zgodę na przetwarzanie danych osobowych w celu realizacji zamówienia zgodnie z Polityką Prywatności. *</span>
</label>
{state.message && !state.success && (
<p className="text-sm text-red-500">{state.message}</p>
)}
<button type="submit" disabled={pending} className="btn btn-primary text-white font-bold w-full flex items-center justify-center gap-2">
{pending ? 'Wysyłanie...' : 'Wyślij zamówienie'}
</button>
<p className="text-xs text-text-muted text-center">
Po złożeniu zamówienia skontaktujemy się z Tobą telefonicznie w ciągu 24 godzin, aby potwierdzić szczegóły i ustalić termin montażu.
</p>
</form>
</div>
</div>
</main>
);
}
+145
View File
@@ -0,0 +1,145 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getProductBySlug, getProducts } from '@/content/products';
import { companyDetails } from '@/content/company';
import Image from 'next/image';
import Link from 'next/link';
import { ArrowLeft, Phone, Zap, Wind, Volume2, Thermometer } from 'lucide-react';
import MultisplitContactForm from '@/components/shop/MultisplitContactForm';
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
return getProducts()
.filter(p => p.category === 'multisplit')
.map(p => ({ slug: p.slug }));
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const product = getProductBySlug(slug);
if (!product) return { title: 'Produkt nie znaleziony' };
return {
title: `Zapytaj o ${product.name}`,
description: `Zapytaj o wycenę systemu multisplit ${product.name}. Bezpłatna konsultacja i wycena montażu.`,
};
}
export default async function MultisplitInquiryPage({ params }: Props) {
const { slug } = await params;
const product = getProductBySlug(slug);
if (!product || product.category !== 'multisplit') notFound();
const specIcons: Record<string, React.ElementType> = {
'Moc chłodnicza': Zap,
'Moc grzewcza': Thermometer,
'Głośność wewnętrzna (dB)': Volume2,
'Klasa energetyczna': Wind,
};
const keySpecs = ['Moc chłodnicza', 'Moc grzewcza', 'Klasa energetyczna', 'Głośność wewnętrzna (dB)'];
return (
<main className="bg-slate-50 min-h-screen">
{/* Breadcrumb */}
<div className="bg-white border-b border-border">
<div className="max-w-5xl mx-auto px-4 py-3">
<nav className="flex items-center gap-1.5 text-xs text-text-muted">
<Link href="/" className="hover:text-accent transition-colors">Strona główna</Link>
<span>/</span>
<Link href="/sklep" className="hover:text-accent transition-colors">Sklep</Link>
<span>/</span>
<Link href="/sklep?category=multisplit" className="hover:text-accent transition-colors">Multisplit</Link>
<span>/</span>
<span className="text-primary font-medium truncate max-w-[200px]">{product.name}</span>
</nav>
</div>
</div>
<div className="max-w-5xl mx-auto px-4 py-10">
<Link href="/sklep?category=multisplit" className="inline-flex items-center gap-1.5 text-sm text-text-muted hover:text-accent transition-colors mb-6">
<ArrowLeft className="size-4" /> Wróć do multisplitów
</Link>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-10">
{/* Lewa — zdjęcie + specyfikacja */}
<div className="flex flex-col gap-4">
<div className="bg-white rounded-2xl border border-border p-6">
<div className="flex items-center justify-between mb-4">
<span className="text-xs font-bold text-white bg-accent px-2.5 py-1 rounded-full">Multisplit</span>
<span className="text-xs text-text-muted">{product.brand}</span>
</div>
<div className="relative w-full h-56 mb-4 bg-slate-100 rounded-xl flex items-center justify-center">
{product.image && !product.image.includes('placeholder') ? (
<Image
src={product.image}
alt={product.name}
fill
className="object-contain"
/>
) : (
<div className="flex flex-col items-center gap-2 text-text-muted">
<Wind className="size-12 text-slate-300" />
<span className="text-xs">Zdjęcie wkrótce</span>
</div>
)}
</div>
<h1 className="text-lg font-extrabold text-primary font-heading mb-1">{product.name}</h1>
{product.shortDescription && (
<p className="text-sm text-text-muted">{product.shortDescription}</p>
)}
</div>
{/* Kluczowe parametry */}
<div className="bg-white rounded-2xl border border-border p-5">
<h2 className="text-sm font-bold text-primary mb-3">Kluczowe parametry</h2>
<div className="grid grid-cols-2 gap-3">
{keySpecs.map(key => {
const val = product.specs?.[key];
if (!val) return null;
const Icon = specIcons[key] || Zap;
return (
<div key={key} className="flex items-start gap-2 p-3 bg-slate-50 rounded-xl">
<Icon className="size-4 text-accent shrink-0 mt-0.5" />
<div>
<p className="text-xs text-text-muted">{key}</p>
<p className="text-sm font-bold text-primary">{val}</p>
</div>
</div>
);
})}
</div>
</div>
{/* Wycena indywidualna info */}
<div className="bg-accent/10 border border-accent/20 rounded-2xl p-5">
<p className="text-sm font-bold text-accent mb-1">Wycena indywidualna</p>
<p className="text-xs text-text-muted leading-relaxed">
Cena systemu multisplit zależy od liczby pomieszczeń, długości instalacji i warunków montażu.
Wypełnij formularz — oddzwonimy z bezpłatną wyceną w ciągu 24 godzin.
</p>
<a href={`tel:${companyDetails.phoneRaw}`} className="inline-flex items-center gap-1.5 mt-3 text-sm font-bold text-accent hover:underline">
<Phone className="size-4" /> {companyDetails.phone}
</a>
</div>
</div>
{/* Prawa — formularz */}
<div>
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-extrabold text-primary font-heading mb-1">
Zapytaj o wycenę
</h2>
<p className="text-sm text-text-muted mb-6">
Wypełnij formularz — skontaktujemy się w ciągu 24 godzin z bezpłatną wyceną montażu.
</p>
<MultisplitContactForm productName={product.name} productSku={product.sku} />
</div>
</div>
</div>
</div>
</main>
);
}
+105
View File
@@ -0,0 +1,105 @@
"use server";
import { z } from "zod";
import { headers } from "next/headers";
import { escapeHtml, rateLimit, verifyRecaptcha, baseContactSchema } from "@/lib/security";
import { getMailClient } from "@/lib/mail";
const multisplitFormSchema = baseContactSchema.extend({
city: z.string().min(1, "Wybierz miasto"),
rooms: z.string().optional(),
message: z.string().optional(),
productName: z.string(),
productSku: z.string(),
recaptchaToken: z.string().optional(),
});
export async function submitMultisplitForm(data: Record<string, unknown>) {
try {
const ip = (await headers()).get('x-forwarded-for') || 'unknown';
if (!rateLimit(ip)) {
return { success: false, message: 'Wysłano zbyt wiele zapytań. Spróbuj ponownie później.' };
}
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && process.env.RECAPTCHA_SECRET_KEY) {
if (!data.recaptchaToken || typeof data.recaptchaToken !== 'string') {
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
}
const success = await verifyRecaptcha(data.recaptchaToken);
if (!success) {
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
}
}
const mailClient = getMailClient();
if (!mailClient) {
return { success: false, message: "Funkcja wysyłania wiadomości jest tymczasowo niedostępna." };
}
// Convert boolean consent to string for base schema compatibility if it comes as boolean
const processData = { ...data };
if (typeof processData.consent === 'boolean') {
processData.consent = processData.consent ? "true" : "false";
}
const validatedData = multisplitFormSchema.parse(processData);
const { data: emailData, error } = await mailClient.resend.emails.send({
from: mailClient.fromEmail,
to: mailClient.toEmail,
replyTo: validatedData.email || undefined,
subject: `Zapytanie o multisplit: ${escapeHtml(validatedData.productName)} (${escapeHtml(validatedData.productSku)})`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; background-color: #ffffff;">
<div style="background-color: #0F2A47; padding: 20px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px;">Zapytanie o multisplit</h1>
</div>
<div style="padding: 24px; color: #1e293b;">
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9; width: 120px;"><strong>Imię:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.name)}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Telefon:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.phone)}</td>
</tr>
<tr>
<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;">${escapeHtml(validatedData.email || 'Brak')}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Miasto:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.city)}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Produkt:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.productName)} (${escapeHtml(validatedData.productSku)})</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Pokoje:</strong></td>
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${escapeHtml(validatedData.rooms || '')}</td>
</tr>
</table>
<h2 style="font-size: 18px; border-bottom: 2px solid #f1f5f9; padding-bottom: 10px;">Dodatkowe informacje:</h2>
<div style="background-color: #f8fafc; padding: 15px; border-radius: 6px; font-size: 15px; line-height: 1.6; white-space: pre-wrap;">
${escapeHtml(validatedData.message || 'Brak')}
</div>
</div>
</div>
`,
});
if (error) {
console.error("Resend Error:", error);
return { success: false, message: "Nie udało się wysłać wiadomości ze względu na błąd serwera pocztowego." };
}
console.log("Wysłano e-mail (ID):", emailData?.id);
return { success: true, message: "Dziękujemy za zapytanie! Skontaktujemy się z Tobą najszybciej jak to możliwe." };
} catch (error) {
console.error("Błąd formularza:", error);
return { success: false, message: "Błąd walidacji lub błąd serwera. Spróbuj ponownie później." };
}
}