Zastosowanie wszystkich poprawek UI i logiki kalkulatora
This commit is contained in:
@@ -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 "Z montażem", 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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. "ciasteczka") – 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
+224
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
@@ -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} />;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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.",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}</>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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.' };
|
||||
}
|
||||
}
|
||||
@@ -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}</>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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." };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user