commit 7324a6f800828476247a1982b701f09a9c81cb7a Author: AI Assistant Date: Mon Aug 10 23:26:52 2026 +0200 Zastosowanie wszystkich poprawek UI i logiki kalkulatora diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..64ebdaa --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +RESEND_API_KEY= +RESEND_FROM_EMAIL= +RESEND_TO_EMAIL= +NEXT_PUBLIC_RECAPTCHA_SITE_KEY= +RECAPTCHA_SECRET_KEY= +NEXT_PUBLIC_BASE_URL= diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..666a851 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +*.js +*.cjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5355dde --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# temp files +skipped.json + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..41e1aa8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,109 @@ +# AGENTS.md — Reguły projektu + +> Ten plik definiuje stałe zasady dla każdego agenta pracującego nad projektem. +> Czytaj go na początku każdej sesji i stosuj się do niego bez wyjątku. +> Szczegółowe specyfikacje znajdują się w `docs/specs/` i `docs/pages/`. + +--- + +## 1. Czym jest ten projekt + +Strona firmy z branży HVAC i usług domowych. Firma oferuje: +- **Klimatyzacja** — montaż, serwis, sprzedaż (sklep z koszykiem). +- **Pompy ciepła** — dobór, montaż, dofinansowania. +- **Usługi domowe** — remonty i wykończenia, hydraulika, serwis lodówek/AGD, drobne naprawy. + +Dwa cele biznesowe: (1) generowanie leadów (kontakt/wycena), (2) sprzedaż klimatyzatorów online. +Język interfejsu: **polski**. Grupa docelowa: klienci indywidualni i małe firmy. + +--- + +## 2. Rola agenta + +Działaj jak **Senior Next.js Developer** z dbałością o jakość produkcyjną. +- Przed kodowaniem zadania złożonego — przygotuj plan i listę zadań (tryb Planning). +- Po implementacji UI — uruchom dev server i zweryfikuj w przeglądarce (desktop + mobile). +- Na końcu każdego etapu — krótkie podsumowanie + zrzut ekranu, zanim przejdziesz dalej. +- Nie mieszaj refaktoru z nowymi funkcjami w jednym zadaniu. + +--- + +## 3. Stack technologiczny (obowiązkowy) + +- **Next.js** (najnowsza stabilna) — App Router + Server Components. +- **TypeScript** — tryb strict, bez `any` bez uzasadnienia. +- **Tailwind CSS** — stylowanie, tokeny w configu (patrz `docs/specs/02-design-system.md`). +- **shadcn/ui** — biblioteka komponentów bazowych. +- **lucide-react** — ikony. +- **React Hook Form + Zod** — formularze i walidacja. +- **Zustand** — stan koszyka (lekki, bez Reduxa). +- **next/image**, **next/font** — obrazy i fonty. +- Brak zewnętrznego backendu na tym etapie. Dane = pliki w `content/`. Formularze = Server Actions + (przygotuj miejsce na integrację e-mail, na razie bez realnej wysyłki). + +--- + +## 4. Twarde zasady jakości (nie do pominięcia) + +1. **Architektura** — trzymaj się struktury z `docs/specs/01-architecture.md`. Żadnych plików „na skróty". +2. **Komponenty** — reużywalne, typowane, jedna odpowiedzialność. Header/Footer/sekcje współdzielone, + nigdy kopiowane między stronami. +3. **Server vs Client** — domyślnie Server Components. `"use client"` tylko tam, gdzie potrzebna + interaktywność (formularze, koszyk, dropdowny, slidery). +4. **Responsywność** — mobile-first. Każda strona działa od 360px do 1920px. +5. **Dostępność** — semantyczny HTML, ARIA gdzie trzeba, widoczny focus, kontrast min. WCAG AA. +6. **SEO** — patrz `docs/specs/03-seo.md`. Metadata per strona, jeden H1, JSON-LD gdzie wskazano. +7. **Wydajność** — obrazy zoptymalizowane, lazy loading, minimum JS po stronie klienta. +8. **Build** — `npm run build` musi przechodzić bez błędów i ostrzeżeń TypeScript. + +--- + +## 5. Konwencje kodu + +- Nazwy plików komponentów: PascalCase (`HeroSection.tsx`). Pozostałe: kebab-case. +- Jeden komponent = jeden plik. Eksport nazwany dla komponentów współdzielonych. +- Importy absolutne przez alias `@/` (skonfiguruj w tsconfig). +- Teksty UI po polsku, trzymane blisko komponentu lub w `content/` (nie hardcode w wielu miejscach). +- Komentarze tylko tam, gdzie wyjaśniają „dlaczego", nie „co". +- Każda sekcja strony = osobny komponent w `components/sections/`. + +--- + +## 6. System projektowy (skrót — pełnia w docs/specs/02) + +- Kolory: granat `#0F2A47` (primary), cyjan `#2BA8E0` (akcent), pomarańcz `#F39320` (CTA), + biały + jasnoszary `#F5F7FA` (tła), ciemnoszary tekst. +- Font: Inter lub Poppins (next/font). Duże, wyraźne nagłówki. +- Karty: `rounded-xl`, subtelny cień. Przyciski CTA: pomarańcz z wyraźnym hover. +- Estetyka: czysta, profesjonalna, dużo wolnej przestrzeni. NIE zatłaczaj sekcji. +- Spójność: identyczny header, footer i styl na wszystkich stronach. + +--- + +## 7. Kolejność realizacji (etapy) + +Realizuj projekt etapami. Nie przeskakuj — każdy etap bazuje na poprzednim. + +| Etap | Zakres | Dokumentacja | +|------|--------|--------------| +| 1 | Setup: Next.js+TS+Tailwind+shadcn/ui+fonty+tokeny | `docs/specs/01`, `02` | +| 2 | Layout wspólny: header (dropdowny, mobile menu), footer, pasek zaufania | `docs/specs/04-layouts.md` | +| 3 | Strona główna (wszystkie sekcje jako komponenty) | `docs/pages/01-home.md` | +| 4 | Strony usługowe: klimatyzacja, pompy ciepła, usługi domowe + 3 podstrony | `docs/pages/02–07` | +| 5 | Realizacje, blog (+ [slug]), o nas, kontakt (formularz) | `docs/pages/08–11` | +| 6 | E-commerce: sklep, produkt, koszyk, raty | `docs/pages/12–15` | +| 7 | Strony formalne + finalne SEO/JSON-LD | `docs/pages/16`, `docs/specs/03` | +| 8 | Build produkcyjny, naprawa błędów, weryfikacja w przeglądarce | — | + +--- + +## 8. Jak czytać dokumentację + +- `docs/specs/01-architecture.md` — struktura katalogów, routing, wzorce. +- `docs/specs/02-design-system.md` — pełne tokeny, typografia, komponenty UI. +- `docs/specs/03-seo.md` — metadata, JSON-LD, sitemap. +- `docs/specs/04-layouts.md` — header, footer, nawigacja, wspólne elementy. +- `docs/specs/05-data-model.md` — modele danych i mock content. +- `docs/pages/NN-*.md` — szczegółowy brief każdej strony (sekcje, komponenty, treść). + +Gdy budujesz daną stronę — najpierw przeczytaj jej plik w `docs/pages/`, potem odpowiednie specs. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..83c4441 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Paczka promptów — strona HVAC w Next.js (dla Google Antigravity) + +Kompletna dokumentacja projektowa rozbita na warstwy. Antigravity czyta `AGENTS.md` +automatycznie i traktuje go jako stałe reguły; reszta to szczegółowe specyfikacje, do których +agent sięga podczas pracy nad konkretnym etapem/stroną. + +## Jak użyć + +1. Utwórz folder nowego projektu i skopiuj do niego całą tę zawartość + (`AGENTS.md` w katalogu głównym, folder `docs/` obok). +2. Otwórz folder w Antigravity. +3. Agent Manager → New Task → tryb **Planning** (projekt jest wielo-plikowy). +4. Pierwszy prompt: + > Przeczytaj `AGENTS.md` i całą dokumentację w `docs/`. Przygotuj plan implementacji + > i listę zadań zgodnie z etapami z sekcji 7 AGENTS.md. Zacznij od Etapu 1 (setup). + > Po każdym etapie pokaż podsumowanie i zrzut ekranu, zanim przejdziesz dalej. +5. Przejrzyj wygenerowany plan/Task List PRZED akceptacją. Potem prowadź etap po etapie. + +## Struktura paczki + +``` +AGENTS.md # reguły projektu (czytane automatycznie) +docs/ +├── specs/ +│ ├── 01-architecture.md # struktura katalogów, routing, wzorce +│ ├── 02-design-system.md # kolory, typografia, komponenty UI +│ ├── 03-seo.md # metadata, JSON-LD, sitemap +│ ├── 04-layouts.md # header, footer, nawigacja, CTA +│ └── 05-data-model.md # typy i mock content +└── pages/ + ├── 01-home.md + ├── 02-klimatyzacja.md + ├── 03-pompy-ciepla.md + ├── 04-uslugi-domowe.md + ├── 05-07-podstrony-uslug.md # remonty, hydraulika, serwis AGD + ├── 08-11-realizacje-blog-onas-kontakt.md + ├── 12-15-ecommerce.md # sklep, produkt, koszyk, raty + └── 16-strony-formalne.md +``` + +## Kolejność pracy +Trzymaj się etapów z `AGENTS.md` (sekcja 7). Nie buduj wszystkiego naraz — +każdy etap bazuje na poprzednim, a weryfikacja w przeglądarce po etapie łapie błędy wcześnie. + +## Co uzupełnić po wygenerowaniu (właściciel) +- Dane firmy (adres, NIP, telefon, godziny) — `content/company.ts`. +- Zdjęcia (hero, realizacje, produkty) — w miejscach oznaczonych `TODO`. +- Treści prawne — strony formalne. +- Realne ceny i produkty — `content/products.ts`, `content/pricing.ts`. +- Integracja wysyłki formularza (np. Resend) i ewentualny realny checkout. diff --git a/app/(formalne)/dostawa/page.tsx b/app/(formalne)/dostawa/page.tsx new file mode 100644 index 0000000..d867827 --- /dev/null +++ b/app/(formalne)/dostawa/page.tsx @@ -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 ( + <> +

Dostawa i Płatności

+ +

+ Sprawdź dostępne metody wysyłki urządzeń oraz formy płatności akceptowane w naszym sklepie. +

+ + {/* TODO: treść prawna do uzupełnienia przez właściciela */} + +

1. Metody i koszty dostawy

+

Dla wygody naszych klientów przygotowaliśmy kilka opcji odbioru towaru:

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Opcja dostawyCzas realizacjiKoszt brutto
Kurier Paletowy (Klimatyzatory)2-3 dni robocze0 zł (Darmowa)
Kurier Standard (Mniejsze elementy)1-2 dni robocze19 zł
Dostawa przez naszych InstalatorówW dniu montażu0 zł
+
+ +

2. Dostawa podczas montażu

+

+ 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. +

+ +

3. Metody płatności

+ + + ); +} diff --git a/app/(formalne)/gwarancja/page.tsx b/app/(formalne)/gwarancja/page.tsx new file mode 100644 index 0000000..48765e8 --- /dev/null +++ b/app/(formalne)/gwarancja/page.tsx @@ -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 ( + <> +
+ +

Gwarancja i Serwis

+
+ +

+ Jesteśmy autoryzowanym partnerem wszystkich sprzedawanych przez nas urządzeń, co oznacza pełne bezpieczeństwo Twojej inwestycji i bezproblemową realizację świadczeń gwarancyjnych. +

+ + {/* TODO: treść prawna do uzupełnienia przez właściciela */} + +

1. Gwarancja na urządzenia

+

Czas trwania gwarancji zależy od wybranego producenta:

+ + +
+ Ważne: Warunkiem zachowania gwarancji jest wykonywanie regularnych (płatnych) przeglądów technicznych zgodnie z zaleceniami producenta – zazwyczaj dwa razy do roku. +
+ +

2. Gwarancja na usługę montażu

+

+ Bierzemy pełną odpowiedzialność za wykonaną przez nas pracę. Udzielamy 5-letniej rękojmi 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. +

+ +

3. Autoryzowany Serwis

+

+ 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. +

+ +

4. Zleć nam przegląd gwarancyjny

+

+ 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. +

+ Skontaktuj się z nami, aby zarezerwować termin wizyty serwisowej. +

+ + ); +} diff --git a/app/(formalne)/layout.tsx b/app/(formalne)/layout.tsx new file mode 100644 index 0000000..d8818cb --- /dev/null +++ b/app/(formalne)/layout.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export default function FormalneLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+
+ {children} +
+
+
+ ); +} diff --git a/app/(formalne)/polityka-prywatnosci/page.tsx b/app/(formalne)/polityka-prywatnosci/page.tsx new file mode 100644 index 0000000..ff5b10d --- /dev/null +++ b/app/(formalne)/polityka-prywatnosci/page.tsx @@ -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 ( + <> +

Polityka Prywatności i Cookies

+ +
+

+ 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. +

+ +

1. Administrator danych

+

+ Administratorem Twoich danych osobowych jest {companyDetails.legalName} z siedzibą we Wrocławiu (adres: {companyDetails.address.street}, {companyDetails.address.zipCode} {companyDetails.address.city}), NIP: {companyDetails.nip}, REGON: {companyDetails.regon}. +

+

Z administratorem można skontaktować się za pośrednictwem adresu e-mail: {companyDetails.email}.

+ +

2. Zakres i cele przetwarzania

+

Przetwarzamy dane osobowe podane przez Ciebie dobrowolnie w celu:

+ + +

3. Prawa Użytkownika

+

Zgodnie z przepisami RODO masz prawo do:

+ + +

4. Pliki Cookies i Analityka (Google Analytics 4 / Tag Manager)

+

+ 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. +

+ +

Google Analytics 4 (GA4) i Google Tag Manager (GTM)

+

+ 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: Google Analytics 4 oraz Google Tag Manager. +

+ + +

5. Okres przechowywania danych

+

+ 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). +

+ +

+ Ostatnia aktualizacja dokumentu: {new Date().toLocaleDateString('pl-PL')} r. +

+
+ + ); +} diff --git a/app/(formalne)/regulamin/page.tsx b/app/(formalne)/regulamin/page.tsx new file mode 100644 index 0000000..722dc50 --- /dev/null +++ b/app/(formalne)/regulamin/page.tsx @@ -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 ( + <> +

Regulamin Serwisu i Sklepu Internetowego

+ +

+ Niniejszy dokument określa zasady korzystania z serwisu AirProject oraz dokonywania w nim zakupów urządzeń i usług montażu. +

+ + {/* TODO: treść prawna do uzupełnienia przez właściciela i weryfikacji przez radcę prawnego */} + +

§ 1. Postanowienia ogólne

+

+ 1. Sklep internetowy działający pod adresem internetowym airproject.pl prowadzony jest przez: + [TUTAJ WPISZ DANE FIRMY: Nazwa, Adres, NIP, REGON]. +

+

+ 2. Niniejszy Regulamin określa zasady zawierania umów sprzedaży i świadczenia usług za pośrednictwem serwisu. +

+ +

§ 2. Definicje

+ + +

§ 3. Zasady składania i realizacji zamówień

+

+ 1. Informacje zawarte na Stronie Internetowej Sklepu nie stanowią oferty w rozumieniu Kodeksu Cywilnego, a jedynie zaproszenie do zawarcia umowy. +
+ 2. Klient może składać zamówienia 24 godziny na dobę przez 7 dni w tygodniu. +
+ 3. Realizacja usług montażowych odbywa się po uprzednim kontakcie i ustaleniu terminu. +

+ +

§ 4. Płatności i ceny

+

+ 1. Wszystkie ceny na stronie są cenami brutto (zawierają podatek VAT). +

+ +

§ 5. Odstąpienie od umowy

+

+ 1. Konsument ma prawo odstąpić od umowy zawartej na odległość bez podania przyczyny w terminie 14 dni od dnia dostarczenia towaru. +
+ 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. +

+ +

+ Ostatnia aktualizacja regulaminu: 1 stycznia 2026 r. +

+ + ); +} diff --git a/app/(formalne)/reklamacje/page.tsx b/app/(formalne)/reklamacje/page.tsx new file mode 100644 index 0000000..97495b1 --- /dev/null +++ b/app/(formalne)/reklamacje/page.tsx @@ -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 ( + <> +

Reklamacje i Zwroty

+ +

+ 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. +

+ + {/* TODO: treść prawna do uzupełnienia przez właściciela */} + +

1. Zwroty (Odstąpienie od umowy)

+

+ 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: +

+ + +

Jak dokonać zwrotu?

+
    +
  1. Powiadom nas drogą mailową na adres: kontakt@twoj-sklep.pl.
  2. +
  3. Zapakuj bezpiecznie produkt w oryginalne opakowanie.
  4. +
  5. Odeślij go na wskazany przez nas adres magazynu.
  6. +
  7. Po weryfikacji sprzętu, środki zwrócimy Ci w ciągu 14 dni roboczych.
  8. +
+ +

2. Procedura Reklamacyjna (Rękojmia i Gwarancja)

+

+ 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 Gwarancja). +

+ +

Jak zgłosić usterkę?

+

+ W razie awarii sprzętu zamontowanego przez naszą firmę, zalecamy kontakt telefoniczny lub poprzez e-mail: +

+ + +
+

Masz dodatkowe pytania?

+

Zadzwoń do nas pod numer {companyDetails.phone} lub skorzystaj z formularza kontaktowego.

+
+ + ); +} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..e9d8b6b --- /dev/null +++ b/app/blog/[slug]/page.tsx @@ -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 { + 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 = { + '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 ( +
+ {/* Skrypty JSON-LD dla SEO */} + + + +
+ {/* Powrót */} + + + Wróć do listy artykułów + + + {/* Header artykułu */} +
+
+ + {categoryLabels[post.category] || post.category} + +
+ + +
+ {post.author && ( +
+ + {post.author} +
+ )} +
+ +

+ {post.title} +

+ +

+ {post.excerpt} +

+
+ + {/* Główny obraz */} +
+ {post.title} +
+ + {/* Treść */} +
+ {/* 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. */} +
+
+ +
+ + {/* CTA do usługi */} + + + {/* Powiązane artykuły */} + {relatedPosts.length > 0 && ( +
+
+

Powiązane artykuły

+ + Więcej wpisów + +
+ +
+ {relatedPosts.map(related => ( + +
+ {related.title} +
+

+ {related.title} +

+

+ {related.excerpt} +

+ + ))} +
+
+ )} +
+ ); +} diff --git a/app/blog/page.tsx b/app/blog/page.tsx new file mode 100644 index 0000000..6f60c4f --- /dev/null +++ b/app/blog/page.tsx @@ -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 ( +
+ + +
+ +
+ + +
+ ); +} diff --git a/app/cennik-montazu/page.tsx b/app/cennik-montazu/page.tsx new file mode 100644 index 0000000..e930449 --- /dev/null +++ b/app/cennik-montazu/page.tsx @@ -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 ( + <> + +
+ {/* Hero */} +
+ Cennik montażu klimatyzacji Wrocław +
+
+
+ + Aktualne ceny — 2026 +
+

+ Cennik montażu klimatyzacji
+ Wrocław — bez ukrytych kosztów +

+

+ Przejrzyste ceny montażu klimatyzacji. Przed każdą realizacją wystawiamy + bezpłatną wycenę — płacisz dokładnie tyle ile uzgodniono. Zero niespodzianek. +

+
+ + + + + + Formularz kontaktowy + + +
+
+
+ + {/* Cennik główny */} +
+
+ + {/* Montaż */} +
+
+ +

Montaż klimatyzacji

+
+
+ {CENNIK_MONTAZ.map((r, i) => ( +
0 ? 'border-t border-border' : ''} ${r.highlight ? 'bg-accent/5' : 'bg-white'}`}> +
+ {r.highlight && } + {r.label} +
+ {r.cena} +
+ ))} +
+
+ + {/* Trasa freonowa */} +
+
+ +

Trasa freonowa i dodatkowe prace

+
+
+ {CENNIK_TRASA.map((r, i) => ( +
0 ? 'border-t border-border' : ''}`}> + {r.label} + {r.cena} +
+ ))} +
+
+

+ Bruzda vs korytko: 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. +

+
+
+ + {/* Serwis */} +
+
+ +

Serwis i przeglądy

+
+
+ {CENNIK_SERWIS.map((r, i) => ( +
0 ? 'border-t border-border' : ''}`}> + {r.label} + {r.cena} +
+ ))} +
+
+ +

+ * 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ą. +

+
+
+ + {/* FAQ */} +
+
+

Często zadawane pytania o cennik

+
+ {faqItems.map((item, i) => ( +
+ + {item.q} + + +
+ {item.a} +
+
+ ))} +
+
+
+ + {/* CTA */} +
+
+

+ Chcesz poznać dokładną cenę montażu? +

+

+ Zadzwoń lub napisz — bezpłatna wycena w ciągu 24 godzin. Obsługujemy Wrocław i cały Dolny Śląsk. +

+
+ + + + + + Napisz do nas + + +
+
+
+
+ + ); +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..a8bd557 Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..5662945 --- /dev/null +++ b/app/globals.css @@ -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; + } +} diff --git a/app/icon.png b/app/icon.png new file mode 100644 index 0000000..a8bd557 Binary files /dev/null and b/app/icon.png differ diff --git a/app/kalkulator/page.tsx b/app/kalkulator/page.tsx new file mode 100644 index 0000000..6e8b663 --- /dev/null +++ b/app/kalkulator/page.tsx @@ -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 ; +} diff --git a/app/klimatyzacja/page.tsx b/app/klimatyzacja/page.tsx new file mode 100644 index 0000000..e73230f --- /dev/null +++ b/app/klimatyzacja/page.tsx @@ -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 ( + <> + +
+ + + + + + + + + + + + + + + + + + + + + {/* Sekcja miast dla SEO */} +
+
+
+ + Zasięg działania + +

+ Montaż klimatyzacji na Dolnym Śląsku +

+

+ Działamy na terenie całego Dolnego Śląska. Wybierz swoje miasto i dowiedz się więcej: +

+
+
+ {dolnySlaskCities.map((city) => ( + + Klimatyzacja {city.name} + + ))} +
+
+
+ + +
+ + ); +} diff --git a/app/kontakt/actions.ts b/app/kontakt/actions.ts new file mode 100644 index 0000000..14b70f5 --- /dev/null +++ b/app/kontakt/actions.ts @@ -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 || "noreply@thermcool.pl", + to: process.env.RESEND_TO_EMAIL || "biuro@thermcool.pl", // Docelowy e-mail (odbiorca) + replyTo: validatedData.email, + subject: `Nowa wiadomość ze strony: ${validatedData.subject}`, + html: ` +
+
+

Nowe zapytanie z formularza

+

Domena: thermcool.pl

+
+
+

Dane kontaktowe:

+ + + + + + + + + + + + + + + + + + + + + +
Imię:${escapeHtml(validatedData.name)}
Telefon:${escapeHtml(validatedData.phone)}
E-mail:${escapeHtml(validatedData.email || '')}
Miasto:${escapeHtml(validatedData.city)}
Temat:${escapeHtml(validatedData.subject || '')}
+ +

Treść wiadomości:

+
+ ${escapeHtml(validatedData.message || '')} +
+ +

+ Zgoda RODO została zaakceptowana: ${validatedData.consent ? "Tak" : "Nie"} +

+
+
+ Wiadomość wygenerowana automatycznie z formularza kontaktowego na stronie thermcool.pl +
+
+ `, + }); + + 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.", + }; + } +} diff --git a/app/kontakt/page.tsx b/app/kontakt/page.tsx new file mode 100644 index 0000000..fd06bab --- /dev/null +++ b/app/kontakt/page.tsx @@ -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 ( +
+ + + + + +
+ ); +} diff --git a/app/koszyk/layout.tsx b/app/koszyk/layout.tsx new file mode 100644 index 0000000..d15a797 --- /dev/null +++ b/app/koszyk/layout.tsx @@ -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}; +} diff --git a/app/koszyk/page.tsx b/app/koszyk/page.tsx new file mode 100644 index 0000000..f9eb48a --- /dev/null +++ b/app/koszyk/page.tsx @@ -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 ( +
+
+ +

Koszyk jest pusty

+

Dodaj produkty ze sklepu aby złożyć zamówienie.

+ +Przejdź do sklepu + +
+
+ ); + } + + return ( +
+
+

Twój koszyk

+ +
+ {/* Lista produktów */} +
+ {items.map((item) => ( +
+
+ {item.name} +
+
+

{item.brand}

+

{item.name}

+

{item.price.toLocaleString('pl-PL')} zł

+
+
+ + {item.quantity} + +
+

+ {(item.price * item.quantity).toLocaleString('pl-PL')} zł +

+ +
+ ))} +
+ + {/* Podsumowanie */} +
+
+

Podsumowanie

+
+ Produkty ({items.reduce((s, i) => s + i.quantity, 0)} szt.) + {total.toLocaleString('pl-PL')} zł +
+
+ Montaż + wycena telefoniczna +
+
+
+ Razem (urządzenia) + {total.toLocaleString('pl-PL')} zł +
+

+ koszt montażu ustalany indywidualnie

+
+ + + Złóż zamówienie + + + +
+
+
+
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..dcb9d9a --- /dev/null +++ b/app/layout.tsx @@ -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 ( + + + + + + Przejdź do głównej treści + + {process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && ( + + + {/* Microsoft Clarity */} + + + ); +} diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx new file mode 100644 index 0000000..4c731b1 --- /dev/null +++ b/components/ui/Button.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; + +export type ButtonVariant = 'primary' | 'outline' | 'error' | 'outline-light'; +export type ButtonSize = 'sm' | 'md' | 'lg'; + +export interface ButtonClassesProps { + variant?: ButtonVariant; + size?: ButtonSize; + className?: string; +} + +export function buttonClasses({ variant = 'primary', size = 'md', className }: ButtonClassesProps = {}) { + const baseClasses = 'inline-flex items-center justify-center gap-2 whitespace-nowrap transition-colors'; + + const variantClasses = { + primary: 'btn btn-primary text-white font-bold', + outline: 'btn bg-white/10 text-white border border-white/20 hover:bg-white/20 font-bold', + 'outline-light': 'btn bg-white text-primary border border-border hover:bg-slate-50 font-bold', + error: 'btn btn-error text-white font-bold', + }; + + const sizeClasses = { + sm: 'btn-sm text-sm', + md: '', // default DaisyUI btn size + lg: 'btn-lg text-lg', + }; + + return cn( + baseClasses, + variantClasses[variant], + sizeClasses[size], + className + ); +} + +export interface ButtonProps extends React.ButtonHTMLAttributes, ButtonClassesProps { + children: React.ReactNode; +} + +export const Button = React.forwardRef( + ({ className, variant, size, children, ...props }, ref) => { + return ( + + ); + } +); + +Button.displayName = 'Button'; diff --git a/components/ui/CookieSettingsFloatingIcon.tsx b/components/ui/CookieSettingsFloatingIcon.tsx new file mode 100644 index 0000000..3c8ebe5 --- /dev/null +++ b/components/ui/CookieSettingsFloatingIcon.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { Cookie } from 'lucide-react'; +import { useConsentStore } from '@/lib/consent-store'; + +export function CookieSettingsFloatingIcon() { + const { isInitialized, isOpen, openBanner } = useConsentStore(); + + // Only show if initialized (user has made a decision or default exists) and the banner is closed + if (!isInitialized || isOpen) return null; + + return ( + + ); +} diff --git a/components/ui/CookieSettingsTrigger.tsx b/components/ui/CookieSettingsTrigger.tsx new file mode 100644 index 0000000..56df041 --- /dev/null +++ b/components/ui/CookieSettingsTrigger.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { useConsentStore } from "@/lib/consent-store"; + +export function CookieSettingsTrigger() { + const openBanner = useConsentStore((state) => state.openBanner); + + return ( + + ); +} diff --git a/components/ui/JsonLd.tsx b/components/ui/JsonLd.tsx new file mode 100644 index 0000000..e59f9a5 --- /dev/null +++ b/components/ui/JsonLd.tsx @@ -0,0 +1,8 @@ +export function JsonLd({ data }: { data: Record }) { + return ( +