feat: complete website implementation

This commit is contained in:
Radosław Smoliński
2026-06-21 21:53:15 +02:00
parent b147112bdc
commit dfd3888517
110 changed files with 18276 additions and 217 deletions
+108 -4
View File
@@ -1,5 +1,109 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
# AGENTS.md — Reguły projektu
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
> 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.
+44 -30
View File
@@ -1,36 +1,50 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Paczka promptów — strona HVAC w Next.js (dla Google Antigravity)
## Getting Started
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ą.
First, run the development server:
## Jak użyć
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
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
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## 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.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
## 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.
+67
View File
@@ -0,0 +1,67 @@
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Dostawa i Płatności | AirProject',
description: 'Koszty, formy i szacowane czasy dostawy urządzeń w sklepie AirProject.',
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>
</>
);
}
+60
View File
@@ -0,0 +1,60 @@
import { Metadata } from 'next';
import Link from 'next/link';
import { ShieldCheck } from 'lucide-react';
export const metadata: Metadata = {
title: 'Gwarancja na Klimatyzatory i Montaż | AirProject',
description: 'Szczegółowe informacje dotyczące warunków gwarancji na instalowane urządzenia oraz usługi montażowe.',
alternates: {
canonical: '/gwarancja',
}
};
export default function GwarancjaPage() {
return (
<>
<div className="flex items-center gap-4 mb-6 text-green-600">
<ShieldCheck className="w-12 h-12" />
<h1 className="text-4xl font-extrabold m-0 text-primary">Gwarancja i Serwis</h1>
</div>
<p className="lead">
Jesteśmy autoryzowanym partnerem wszystkich sprzedawanych przez nas urządzeń, co oznacza pełne bezpieczeństwo Twojej inwestycji i bezproblemową realizację świadczeń gwarancyjnych.
</p>
{/* TODO: treść prawna do uzupełnienia przez właściciela */}
<h2 id="gwarancja-urzadzenia">1. Gwarancja na urządzenia</h2>
<p>Czas trwania gwarancji zależy od wybranego producenta:</p>
<ul>
<li><strong>Daikin</strong> – 3 lata gwarancji</li>
<li><strong>Rotenso</strong> – 5 lat gwarancji</li>
<li><strong>Mitsubishi Electric</strong> – 5 lat gwarancji</li>
<li><strong>Gree</strong> – 5 lat gwarancji</li>
<li><strong>LG</strong> – do 10 lat gwarancji na sprężarkę, 3 lata na urządzenie</li>
</ul>
<div className="bg-primary-light/10 border-l-4 border-primary p-4 my-8 rounded-r-lg">
<strong>Ważne:</strong> Warunkiem zachowania gwarancji jest wykonywanie regularnych (płatnych) przeglądów technicznych zgodnie z zaleceniami producenta – zazwyczaj dwa razy do roku.
</div>
<h2 id="gwarancja-montaz">2. Gwarancja na usługę montażu</h2>
<p>
Bierzemy pełną odpowiedzialność za wykonaną przez nas pracę. Udzielamy <strong>5-letniej rękojmi</strong> na poprawność działania całej instalacji chłodniczej i odpływu skroplin. Jeśli jakikolwiek problem wynika z błędu montażowego, naprawimy go na własny koszt.
</p>
<h2 id="serwis">3. Autoryzowany Serwis</h2>
<p>
Posiadamy pełne zaplecze techniczne, własnych wykwalifikowanych serwisantów i uprawnienia UDT oraz certyfikaty F-GAZ. Co to oznacza dla Ciebie?
W przypadku awarii, nie odsyłamy Cię do producenta. Przyjeżdżamy na miejsce diagnozując usterkę, a w razie konieczności zamawiamy części i realizujemy naprawę, oszczędzając Twój czas i nerwy.
</p>
<h2 id="przeglady">4. Zleć nam przegląd gwarancyjny</h2>
<p>
Oferujemy atrakcyjne pakiety stałej opieki serwisowej. Przy stałej współpracy koszty serwisu są niższe, a my sami pilnujemy za Ciebie terminów obowiązkowych przeglądów narzucanych przez producentów sprzętu.
<br/><br/>
<Link href="/kontakt" className="font-bold text-accent hover:underline">Skontaktuj się z nami</Link>, aby zarezerwować termin wizyty serwisowej.
</p>
</>
);
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
export default function FormalneLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<main className="bg-slate-50 min-h-screen py-24 md:py-32">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="bg-white p-8 md:p-16 rounded-3xl border border-border shadow-sm prose prose-slate max-w-none prose-headings:font-heading prose-headings:text-primary prose-a:text-accent hover:prose-a:text-accent-hover prose-a:font-semibold">
{children}
</div>
</div>
</main>
);
}
@@ -0,0 +1,56 @@
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Polityka Prywatności i Cookies | AirProject',
description: 'Zasady przetwarzania danych osobowych oraz wykorzystania plików cookies w serwisie AirProject.',
alternates: {
canonical: '/polityka-prywatnosci',
}
};
export default function PolitykaPrywatnosciPage() {
return (
<>
<h1 className="text-4xl font-extrabold mb-8">Polityka Prywatności i Cookies</h1>
<p className="lead">
Twoja prywatność jest dla nas ważna. W tym dokumencie opisujemy, jakie dane zbieramy, w jakim celu je wykorzystujemy oraz jakie masz w związku z tym prawa.
</p>
{/* TODO: treść prawna do uzupełnienia przez właściciela i weryfikacji z inspektorem ochrony danych / radcą prawnym */}
<h2 id="administrator">1. Administrator danych</h2>
<p>
Administratorem Twoich danych osobowych jest:
<strong> [TUTAJ WPISZ DANE FIRMY: Nazwa, Adres, NIP, e-mail kontaktowy]</strong>.
</p>
<h2 id="zakres-cel">2. Zakres i cele przetwarzania</h2>
<p>Przetwarzamy dane osobowe podane przez Ciebie w formularzu kontaktowym oraz w procesie składania zamówienia w celu:</p>
<ul>
<li>Realizacji zamówień i usług (np. dojazdu instalatorów na wskazany adres).</li>
<li>Komunikacji (odpowiedzi na zapytania z formularza).</li>
<li>Celów księgowych i podatkowych.</li>
</ul>
<h2 id="prawa">3. Twoje prawa</h2>
<p>Zgodnie z przepisami RODO masz prawo do:</p>
<ul>
<li>Dostępu do swoich danych oraz otrzymania ich kopii.</li>
<li>Sprostowania (poprawienia) swoich danych.</li>
<li>Usunięcia danych lub ograniczenia przetwarzania.</li>
<li>Przenoszenia danych.</li>
<li>Wniesienia sprzeciwu wobec przetwarzania.</li>
</ul>
<h2 id="cookies">4. Pliki Cookies</h2>
<p>
Nasza strona wykorzystuje pliki cookies (ciasteczka) w celu zapewnienia prawidłowego działania serwisu (np. zapamiętania zawartości koszyka) oraz w celach analitycznych. Możesz zarządzać ustawieniami cookies z poziomu przeglądarki internetowej.
</p>
<p className="text-sm text-text-muted mt-12 pt-8 border-t border-border">
Ostatnia aktualizacja dokumentu: 1 stycznia 2026 r.
</p>
</>
);
}
+66
View File
@@ -0,0 +1,66 @@
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Regulamin Sklepu i Usług | AirProject',
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).
<br />
2. Podatek VAT na urządzenia sprzedawane z usługą montażu dla osób fizycznych w budynkach mieszkalnych o powierzchni do 300 m² rozliczany jest zgodnie z odpowiednimi przepisami podatkowymi (np. stawką 8%).
</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>
</>
);
}
+63
View File
@@ -0,0 +1,63 @@
import { Metadata } from 'next';
import Link from 'next/link';
export const metadata: Metadata = {
title: 'Reklamacje i Zwroty | AirProject',
description: 'Zasady i procedury składania reklamacji oraz zwrotów produktów i usług w sklepie AirProject.',
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 +48 000 000 000 lub skorzystaj z <Link href="/kontakt">formularza kontaktowego</Link>.</p>
</div>
</>
);
}
+252
View File
@@ -0,0 +1,252 @@
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 { CtaBanner } from '@/components/sections/CtaBanner';
import { Calendar, User, ArrowLeft, ArrowRight } from 'lucide-react';
import { companyDetails } from '@/content/company';
interface BlogPostPageProps {
params: {
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',
};
}
return {
title: `${post.title} | Blog AirProject`,
description: post.excerpt,
alternates: {
canonical: `/blog/${post.slug}`,
},
openGraph: {
title: post.title,
description: post.excerpt,
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',
'pompy-ciepla': 'Pompy Ciepła',
'remonty': 'Remonty',
'hydraulika': 'Hydraulika',
'porady': 'Porady'
};
// Ustawienie CTA dopasowanego do kategorii wpisu
const getCtaLink = () => {
switch(post.category) {
case 'klimatyzacja': return '/klimatyzacja';
case 'pompy-ciepla': return '/pompy-ciepla';
case 'remonty': return '/uslugi-domowe/remonty';
case 'hydraulika': return '/uslugi-domowe/hydraulika';
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://aeroservice-demo.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://aeroservice-demo.pl',
},
{
'@type': 'ListItem',
position: 2,
name: 'Blog',
item: 'https://aeroservice-demo.pl/blog',
},
{
'@type': 'ListItem',
position: 3,
name: post.title,
item: `https://aeroservice-demo.pl/blog/${post.slug}`,
},
],
};
return (
<main className="bg-slate-50 pt-24 pb-16">
{/* Skrypty JSON-LD dla SEO */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdArticle) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(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 || 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=1600&q=80'}
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"
>
<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 || 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&q=80'}
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>
);
}
+53
View File
@@ -0,0 +1,53 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { FeaturedPost } from '@/components/sections/FeaturedPost';
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 | AirProject',
description: 'Baza wiedzy na temat klimatyzacji, pomp ciepła i remontów. Czytaj nasze poradniki i bądź na bieżąco z nowościami w branży.',
alternates: {
canonical: '/blog',
}
};
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="https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=1600&q=80"
/>
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-16">
<h2 className="text-2xl font-bold text-primary font-heading mb-8">Artykuł wyróżniony</h2>
<FeaturedPost post={featuredPost} />
</div>
<PostsGrid posts={remainingPosts} />
</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>
);
}
+183 -11
View File
@@ -1,26 +1,198 @@
@import "tailwindcss";
@plugin "daisyui";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@plugin "@tailwindcss/typography";
:root {
--background: #ffffff;
--foreground: #171717;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--color-text: var(--foreground);
--color-text-muted: var(--muted-foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--font-heading: var(--font-heading);
--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);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--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: #2ba8e0;
--accent-foreground: #ffffff;
--accent-light: #e3f4fc;
--cta: #1e293b;
--cta-foreground: #ffffff;
--cta-hover: #0f172a;
--destructive: oklch(0.577 0.245 27.325);
--theme-border: #e2e8f0;
--input: #e2e8f0;
--ring: #2ba8e0;
--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: #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);
--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;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
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;
}
}
+51
View File
@@ -0,0 +1,51 @@
import { CtaBanner } from "@/components/sections/CtaBanner";
import { Wrench, Phone, ArrowRight } from "lucide-react";
import Link from "next/link";
import { companyDetails } from "@/content/company";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Kalkulator - W przygotowaniu | AIR Project",
description: "Kalkulator doboru klimatyzacji i pomp ciepła w przygotowaniu.",
};
export default function KalkulatorPage() {
return (
<main className="flex-1">
<section className="py-24 md:py-32 bg-muted">
<div className="container mx-auto px-4 max-w-3xl text-center">
<div className="w-24 h-24 bg-primary rounded-full flex items-center justify-center mx-auto mb-8 text-white shadow-xl">
<Wrench className="w-12 h-12" />
</div>
<h1 className="text-4xl md:text-5xl font-heading font-bold text-primary mb-6 leading-tight">
Narzędzie w przygotowaniu
</h1>
<p className="text-lg md:text-xl text-muted-foreground mb-10 leading-relaxed">
Pracujemy nad zaawansowanym konfiguratorem, który krok po kroku pomoże Ci dobrać sprzęt i natychmiast oszacuje koszty montażu. Narzędzie zostanie udostępnione już wkrótce!
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<a href={`tel:${companyDetails.phoneRaw}`} className="w-full sm:w-auto">
<button className="btn btn-primary font-bold w-full sm:w-auto text-white flex items-center justify-center gap-2">
<Phone className="size-5" />
<span>Zadzwoń: {companyDetails.phone}</span>
</button>
</a>
<Link href="/kontakt" className="w-full sm:w-auto">
<button className="btn btn-outline btn-primary bg-white font-bold w-full sm:w-auto flex items-center justify-center gap-2">
<span>Napisz do nas</span>
<ArrowRight className="size-5" />
</button>
</Link>
</div>
</div>
</section>
<CtaBanner
title="Skorzystaj z naszej darmowej wyceny od ręki"
subtitle="Zadzwoń do nas lub wypełnij krótki formularz na stronie kontaktu. Przygotujemy dla Ciebie indywidualną, niezobowiązującą i darmową ofertę szybciej, niż myślisz."
ctaText="Przejdź do kontaktu"
/>
</main>
);
}
+93
View File
@@ -0,0 +1,93 @@
import { Metadata } from 'next';
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';
export const metadata: Metadata = {
title: 'Montaż i Serwis Klimatyzacji | AirProject',
description: 'Profesjonalny montaż klimatyzacji typu split i multisplit. Posiadamy uprawnienia F-gaz. Bezpłatny audyt, dobór urządzeń i sprawdzony serwis.',
alternates: {
canonical: '/klimatyzacja',
}
};
export default function KlimatyzacjaPage() {
return (
<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="https://images.unsplash.com/photo-1527689638836-411945a2b57c?w=1600&q=80"
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" />
<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>
);
}
+111
View File
@@ -0,0 +1,111 @@
"use server";
import { z } from "zod";
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
// Schemat walidacji zgodny z tym po stronie klienta
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.string().email("Niepoprawny adres e-mail"),
subject: z.string().min(1, "Wybierz temat"),
message: z.string().min(10, "Wiadomość musi mieć min. 10 znaków"),
consent: z.boolean().refine((val) => val === true, "Zgoda RODO jest wymagana"),
});
export async function submitContactForm(prevState: any, formData: FormData) {
try {
// 1. Walidacja danych z formularza
const rawData = {
name: formData.get("name"),
phone: formData.get("phone"),
email: formData.get("email"),
subject: formData.get("subject"),
message: formData.get("message"),
consent: formData.get("consent") === "true" || formData.get("consent") === "on",
};
const validatedData = contactFormSchema.parse(rawData);
// 2. Wysłanie e-maila przez Resend
const { data, error } = await 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;">${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:${validatedData.phone}" style="color: #0F2A47; text-decoration: none;">${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:${validatedData.email}" style="color: #0F2A47; text-decoration: none;">${validatedData.email}</a></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;">${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;">
${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);
// 3. Zwrotka o sukcesie
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.",
};
}
}
+33
View File
@@ -0,0 +1,33 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { ContactSection } from '@/components/sections/ContactSection';
import { MapEmbed } from '@/components/sections/MapEmbed';
export const metadata: Metadata = {
title: 'Kontakt | AirProject',
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? Planujesz remont? A może masz awarię, która wymaga natychmiastowej interwencji hydraulika? Napisz lub zadzwoń."
ctaText="Zadzwoń do nas"
ctaLink="tel:+48123456789" // TODO: Use real phone from config
secondaryCtaText="Napisz maila"
secondaryCtaLink="mailto:[email protected]" // TODO: Use real email
backgroundImage="https://images.unsplash.com/photo-1516387938699-a93567ec168e?w=1600&q=80"
/>
<ContactSection />
<MapEmbed />
</main>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { CartItems } from '@/components/shop/CartItems';
import { CartSummary } from '@/components/shop/CartSummary';
export const metadata: Metadata = {
title: 'Twój Koszyk | AirProject',
description: 'Przejrzyj swoje zamówienie na klimatyzatory i usługi montażu przed finalizacją zakupów.',
alternates: {
canonical: '/koszyk',
}
};
export default function KoszykPage() {
return (
<main className="bg-slate-50 min-h-screen">
<HeroSection
eyebrow="Sklep"
title="Twój koszyk"
subtitle="Zanim przejdziesz do płatności, sprawdź szczegóły swojego zamówienia. Możesz łatwo zmienić liczbę urządzeń lub dodać profesjonalny montaż dla wybranych pozycji."
ctaText="Wróc do sklepu"
ctaLink="/sklep"
backgroundImage="https://images.unsplash.com/photo-1556740714-a8395b3bf30f?w=1600&q=80"
/>
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12 items-start">
<div className="lg:col-span-2">
<h2 className="text-2xl font-bold text-primary font-heading mb-8">Lista produktów</h2>
<CartItems />
</div>
<div className="lg:col-span-1">
<CartSummary />
</div>
</div>
</section>
</main>
);
}
+32 -12
View File
@@ -1,20 +1,29 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Inter, Poppins } from "next/font/google";
import { Header } from "@/components/layout/Header";
import { TrustBar } from "@/components/layout/TrustBar";
import { Footer } from "@/components/layout/Footer";
import { GoogleAnalytics, GoogleTagManager } from "@next/third-parties/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
const fontSans = Inter({
variable: "--font-sans",
subsets: ["latin", "latin-ext"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
const fontHeading = Poppins({
variable: "--font-heading",
weight: ["400", "500", "600", "700", "800"],
subsets: ["latin", "latin-ext"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: {
default: "Klimatyzacja, pompy ciepła i usługi domowe",
template: "%s | HVAC & Usługi Domowe",
},
description: "Montaż klimatyzacji, pompy ciepła, remonty i hydraulika. Dobór, montaż i serwis. Darmowa konsultacja.",
metadataBase: new URL("http://localhost:3000"),
};
export default function RootLayout({
@@ -24,10 +33,21 @@ export default function RootLayout({
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
lang="pl"
className={`${fontSans.variable} ${fontHeading.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
{process.env.NEXT_PUBLIC_GTM_ID && (
<GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID} />
)}
<body className="min-h-full flex flex-col bg-background text-foreground">
<Header />
<main className="flex-grow">{children}</main>
<TrustBar />
<Footer />
{process.env.NEXT_PUBLIC_GA_ID && (
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
)}
</body>
</html>
);
}
+51
View File
@@ -0,0 +1,51 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { AboutStory } from '@/components/sections/AboutStory';
import { StatsBar } from '@/components/sections/StatsBar';
import { ValuesGrid } from '@/components/sections/ValuesGrid';
import { CertificatesSection } from '@/components/sections/CertificatesSection';
import { Testimonials } from '@/components/sections/Testimonials';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { companyDetails } from '@/content/company';
export const metadata: Metadata = {
title: 'O Nas | AirProject',
description: 'Poznaj zespół specjalistów z AirProject. Zobacz naszą historię, wartości oraz certyfikaty poświadczające najwyższą jakość usług instalacyjnych i remontowych.',
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="https://images.unsplash.com/photo-1504307651254-35680f356f12?w=1600&q=80"
/>
<AboutStory />
<StatsBar />
<ValuesGrid />
<CertificatesSection />
<Testimonials />
<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>
);
}
+61 -59
View File
@@ -1,65 +1,67 @@
import Image from "next/image";
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 { Testimonials } from "@/components/sections/Testimonials";
import { BlogPreview } from "@/components/sections/BlogPreview";
import { CtaBanner } from "@/components/sections/CtaBanner";
import { companyDetails } from "@/content/company";
export default function Home() {
const jsonLd = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": companyDetails.name,
"image": "http://localhost:3000/logo.png",
"telephone": companyDetails.phone,
"email": companyDetails.email,
"address": {
"@type": "PostalAddress",
"streetAddress": companyDetails.address.street,
"addressLocality": companyDetails.address.city,
"postalCode": companyDetails.address.zipCode,
"addressCountry": "PL",
},
"priceRange": "$$",
"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": "State",
"name": companyDetails.address.region,
},
};
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
<>
{/* LocalBusiness Schema for Google Search SEO */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
<HeroSection />
<AdvantagesBar />
<ServicesGrid />
{/* BrandsBar will go here in Group B */}
<FeaturedProducts />
<WhyUs />
{/* ProjectsSection will go here in Group B */}
<Testimonials />
<BlogPreview />
<CtaBanner bgVariant="accent-light" />
</>
);
}
+67
View File
@@ -0,0 +1,67 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { BenefitsGrid } from '@/components/sections/BenefitsGrid';
import { HeatPumpTypes } from '@/components/sections/HeatPumpTypes';
import { UseCases } from '@/components/sections/UseCases';
import { ProcessSteps } from '@/components/sections/ProcessSteps';
import { FundingSection } from '@/components/sections/FundingSection';
import { ProjectsGallery } from '@/components/sections/ProjectsGallery';
import { FaqAccordion } from '@/components/sections/FaqAccordion';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { heatPumpProcessSteps } from '@/content/process';
export const metadata: Metadata = {
title: 'Pompy Ciepła — Montaż i Dobór | AirProject',
description: 'Zmniejsz rachunki za ogrzewanie dzięki pompie ciepła. Dobór urządzeń, profesjonalny montaż i pomoc w uzyskaniu dofinansowania z programu Czyste Powietrze.',
alternates: {
canonical: '/pompy-ciepla',
}
};
export default function PompyCieplaPage() {
return (
<main>
<HeroSection
title="Pompy ciepła — niższe rachunki, niezależność, dofinansowanie"
subtitle="Zainwestuj w nowoczesne i ekologiczne ogrzewanie domu. Pomagamy na każdym kroku: od audytu, przez uzyskanie dotacji, aż po montaż."
ctaText="Darmowa konsultacja"
ctaLink="/kontakt"
secondaryCtaText="Sprawdź dofinansowanie"
secondaryCtaLink="#dofinansowanie"
backgroundImage="https://images.unsplash.com/photo-1584622650111-993a426fbf0a?w=1600&q=80"
/>
<BenefitsGrid />
<HeatPumpTypes />
<UseCases />
<ProcessSteps
steps={heatPumpProcessSteps}
eyebrow="Wdrożenie"
title="Jak wygląda proces instalacji pompy ciepła?"
subtitle="Kompleksowo zajmiemy się całą inwestycją, abyś mógł szybko cieszyć się tanim ogrzewaniem."
/>
<div id="dofinansowanie">
<FundingSection />
</div>
<ProjectsGallery
category="pompy-ciepla"
title="Nasze realizacje pomp ciepła"
description="Zobacz, jak wyglądają kotłownie i jednostki zewnętrzne zamontowane przez nasz zespół."
/>
<FaqAccordion category="pompy-ciepla" />
<CtaBanner
title="Gotowy na niezależność energetyczną?"
subtitle="Skontaktuj się z nami, a przygotujemy dla Ciebie bezpłatny audyt oraz szacunkową kalkulację oszczędności."
ctaText="Zadzwoń do nas"
ctaLink="/kontakt"
/>
</main>
);
}
+41
View File
@@ -0,0 +1,41 @@
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 | AirProject',
description: 'Zobacz nasze wybrane realizacje z zakresu klimatyzacji, pomp ciepła, remontów i usług hydraulicznych.',
alternates: {
canonical: '/realizacje',
}
};
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="https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=1600&q=80"
/>
<ProjectsGrid />
<StatsBar />
<CtaBanner
title="Chcesz uzyskać podobny efekt?"
subtitle="Umów się na bezpłatną konsultację. Doradzimy, przygotujemy projekt i wycenimy prace bez żadnych zobowiązań."
ctaText="Zamów darmową wycenę"
ctaLink="/kontakt"
/>
</main>
);
}
+12
View File
@@ -0,0 +1,12 @@
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
// Możemy tu dodać disallow np. do panelu admina, jeśli by powstał w przyszłości
},
sitemap: 'https://aeroservice-demo.pl/sitemap.xml',
};
}
+54
View File
@@ -0,0 +1,54 @@
import { MetadataRoute } from 'next';
import { getProducts } from '@/content/products';
import { getPosts } from '@/content/posts';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://aeroservice-demo.pl';
// Statyczne trasy (Główne)
const staticRoutes = [
'',
'/klimatyzacja',
'/pompy-ciepla',
'/uslugi-domowe',
'/uslugi-domowe/remonty',
'/uslugi-domowe/hydraulika',
'/uslugi-domowe/serwis-agd',
'/realizacje',
'/blog',
'/o-nas',
'/kontakt',
'/sklep',
'/koszyk',
'/regulamin',
'/polityka-prywatnosci',
'/reklamacje',
'/dostawa',
'/gwarancja',
].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date().toISOString(),
changeFrequency: 'weekly' as const,
priority: route === '' ? 1 : 0.8,
}));
// Dynamiczne produkty
const products = getProducts();
const productRoutes = products.map((product) => ({
url: `${baseUrl}/sklep/${product.slug}`,
lastModified: new Date().toISOString(),
changeFrequency: 'weekly' as const,
priority: 0.9,
}));
// Dynamiczne artykuły na blogu
const posts = getPosts();
const postRoutes = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: post.date, // Używamy daty publikacji jako lastModified
changeFrequency: 'monthly' as const,
priority: 0.7,
}));
return [...staticRoutes, ...productRoutes, ...postRoutes];
}
+149
View File
@@ -0,0 +1,149 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getProducts, getProductBySlug } from '@/content/products';
import { ProductGallery } from '@/components/shop/ProductGallery';
import { ProductInfo } from '@/components/shop/ProductInfo';
import { Tabs } from '@/components/shop/Tabs';
import { ProductCard } from '@/components/shop/ProductCard';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';
interface ProductPageProps {
params: {
slug: string;
};
}
export async function generateStaticParams() {
const products = getProducts();
return products.map((product) => ({
slug: product.slug,
}));
}
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
const { slug } = await params;
const product = getProductBySlug(slug);
if (!product) {
return { title: 'Produkt nie znaleziony' };
}
return {
title: `${product.brand} ${product.name} | Sklep AirProject`,
description: product.description,
alternates: {
canonical: `/sklep/${product.slug}`,
},
openGraph: {
title: `${product.brand} ${product.name}`,
description: product.description,
type: 'website',
images: [
{
url: product.image || '/hero.jpg',
},
],
},
};
}
export default async function ProductPage({ params }: ProductPageProps) {
const { slug } = await params;
const product = getProductBySlug(slug);
if (!product) {
notFound();
}
const allProducts = getProducts();
// Dobieramy 4 podobne produkty
const relatedProducts = allProducts
.filter(p => p.category === product.category && p.slug !== product.slug)
.slice(0, 4);
// JSON-LD
const jsonLdProduct = {
'@context': 'https://schema.org',
'@type': 'Product',
name: `${product.brand} ${product.name}`,
image: product.image,
description: product.description,
brand: {
'@type': 'Brand',
name: product.brand,
},
offers: {
'@type': 'Offer',
url: `https://aeroservice-demo.pl/sklep/${product.slug}`,
priceCurrency: 'PLN',
price: product.price,
itemCondition: 'https://schema.org/NewCondition',
availability: 'https://schema.org/InStock',
},
};
const jsonLdBreadcrumb = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Sklep',
item: 'https://aeroservice-demo.pl/sklep',
},
{
'@type': 'ListItem',
position: 2,
name: product.name,
item: `https://aeroservice-demo.pl/sklep/${product.slug}`,
},
],
};
return (
<main className="bg-slate-50 pt-24 pb-16 min-h-screen">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdProduct) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdBreadcrumb) }}
/>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Nawigacja powrotu */}
<Link
href="/sklep"
className="inline-flex items-center gap-2 text-sm font-semibold text-text-muted hover:text-primary transition-colors mb-8"
>
<ArrowLeft className="w-4 h-4" />
Wróć do sklepu
</Link>
{/* Górna sekcja */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16">
<ProductGallery image={product.image || ''} name={product.name} />
<ProductInfo product={product} />
</div>
{/* Zakładki z detalami */}
<Tabs product={product} />
{/* Podobne produkty */}
{relatedProducts.length > 0 && (
<section className="mt-24">
<h2 className="text-2xl font-bold text-primary font-heading mb-8">Podobne produkty</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{relatedProducts.map(relProduct => (
<ProductCard key={relProduct.slug} product={relProduct} />
))}
</div>
</section>
)}
</div>
</main>
);
}
+46
View File
@@ -0,0 +1,46 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { ProductsGrid } from '@/components/shop/ProductsGrid';
import { getProducts } from '@/content/products';
export const metadata: Metadata = {
title: 'Sklep z klimatyzatorami | AirProject',
description: 'Wybierz idealny klimatyzator do swojego domu. Oferujemy urządzenia renomowanych marek z możliwością profesjonalnego montażu.',
alternates: {
canonical: '/sklep',
}
};
export default function SklepPage() {
const products = getProducts();
return (
<main>
<HeroSection
eyebrow="E-commerce"
title="Sklep z klimatyzatorami"
subtitle="Znajdź urządzenie idealnie dopasowane do Twoich potrzeb. Zamów samo urządzenie lub wybierz kompleksową usługę z naszym profesjonalnym montażem."
ctaText="Skontaktuj się z doradcą"
ctaLink="/kontakt"
backgroundImage="https://images.unsplash.com/photo-1527689638836-411945a2b57c?w=1600&q=80"
/>
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ProductsGrid products={products} />
</section>
{/* SEO Blok na dole */}
<section className="bg-slate-50 py-16 border-t border-border mt-12">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 prose prose-slate">
<h2 className="text-2xl font-bold font-heading text-primary">Klimatyzatory ścienne, multisplit i przenośne</h2>
<p>
Wybór odpowiedniego klimatyzatora to decyzja na lata. W naszym sklepie oferujemy wyłącznie sprawdzone modele od liderów rynku, takich jak Rotenso, Daikin, Mitsubishi, Gree czy LG. Niezależnie od tego, czy szukasz prostego urządzenia ściennego (split), zaawansowanego systemu dla kilku pomieszczeń (multisplit), czy rozwiązania do biura (klimatyzatory kasetonowe), znajdziesz je u nas w atrakcyjnych cenach.
</p>
<p>
Pamiętaj, że gwarancją niezawodności jest nie tylko sam sprzęt, ale przede wszystkim jego <strong>poprawny montaż</strong>. Zachęcamy do wyboru opcji "Kup z montażem" – nasi certyfikowani instalatorzy F-gaz zapewnią pełen profesjonalizm i bezpieczeństwo Twojej nowej instalacji, a Ty zyskasz spokój i pełną gwarancję producenta.
</p>
</div>
</section>
</main>
);
}
+92
View File
@@ -0,0 +1,92 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { ServicesGrid } from '@/components/sections/ServicesGrid';
import { EmergencyBlock } from '@/components/sections/EmergencyBlock';
import { ProcessSteps } from '@/components/sections/ProcessSteps';
import { WhyUs } from '@/components/sections/WhyUs';
import { FaqAccordion } from '@/components/sections/FaqAccordion';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { homeProcessSteps } from '@/content/process';
import { companyDetails } from '@/content/company';
export const metadata: Metadata = {
title: 'Usługi Hydrauliczne i Pogotowie | AirProject',
description: 'Szybkie i rzetelne usługi hydrauliczne. Usuwanie awarii, naprawa wycieków, instalacje wod-kan, podłączanie sprzętu AGD. Pogotowie hydrauliczne 24/7.',
alternates: {
canonical: '/uslugi-domowe/hydraulika',
}
};
const hydraulikaServices = [
{
title: 'Usuwanie awarii',
description: 'Szybko lokalizujemy i usuwamy przecieki, pęknięte rury oraz nieszczelności w instalacjach wodnych i CO.',
icon: 'AlertTriangle',
},
{
title: 'Udrażnianie rur',
description: 'Skuteczne przepychanie zapchanych odpływów, zlewów, toalet oraz głównych pionów kanalizacyjnych.',
icon: 'Waves',
},
{
title: 'Biały montaż',
description: 'Profesjonalny montaż wanien, kabin prysznicowych, umywalek, toalet oraz baterii i paneli natryskowych.',
icon: 'Bath',
},
{
title: 'Nowe instalacje wod-kan',
description: 'Projektowanie i wykonywanie całych instalacji wodno-kanalizacyjnych w nowo budowanych lub remontowanych obiektach.',
icon: 'Droplets',
},
{
title: 'Przeróbki instalacji',
description: 'Przenoszenie punktów wodnych i kanalizacyjnych, np. przy okazji remontu kuchni lub łazienki.',
icon: 'Wrench',
},
{
title: 'Podłączanie sprzętu',
description: 'Zgodne ze sztuką podłączanie pralek, zmywarek oraz kostkarek do lodu, z zachowaniem gwarancji na sprzęt.',
icon: 'WashingMachine',
}
];
export default function HydraulikaPage() {
return (
<main>
<HeroSection
title="Usługi hydrauliczne — instalacje, naprawy, awarie"
subtitle="Twój sprawdzony hydraulik. Zajmujemy się wszystkim: od wymiany uszczelki i montażu baterii, po kompleksowe wykonanie nowej instalacji wod-kan."
ctaText={`Zadzwoń: ${companyDetails.phone}`}
ctaLink={`tel:${companyDetails.phoneRaw}`}
secondaryCtaText="Napisz do nas"
secondaryCtaLink="/kontakt"
backgroundImage="https://images.unsplash.com/photo-1585704032915-c3400ca199e7?w=1600&q=80"
/>
<EmergencyBlock />
<ServicesGrid
services={hydraulikaServices}
eyebrow="Zakres Usług"
title="W czym dokładnie pomożemy?"
/>
<ProcessSteps
steps={homeProcessSteps}
eyebrow="Realizacja"
title="Szybko i bez stresu"
/>
<WhyUs />
<FaqAccordion category="hydraulika" />
<CtaBanner
title="Planujesz remont lub nową instalację?"
subtitle="Zostaw nam numer telefonu, oddzwonimy i omówimy szczegóły zlecenia."
ctaText="Zamów bezpłatną wycenę"
ctaLink="/kontakt"
/>
</main>
);
}
+86
View File
@@ -0,0 +1,86 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { ServicesGrid } from '@/components/sections/ServicesGrid';
import { ProcessSteps } from '@/components/sections/ProcessSteps';
import { WhyUs } from '@/components/sections/WhyUs';
import { ProjectsGallery } from '@/components/sections/ProjectsGallery';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { homeProcessSteps } from '@/content/process';
export const metadata: Metadata = {
title: 'Usługi Domowe — Remonty i Hydraulika | AirProject',
description: 'Zaufaj specjalistom. Wykonujemy remonty, usługi hydrauliczne oraz serwis AGD z gwarancją jakości. Darmowa wycena i szybkie terminy realizacji.',
alternates: {
canonical: '/uslugi-domowe',
}
};
const domoweServices = [
{
title: 'Remonty i wykończenia',
description: 'Kompleksowe remonty mieszkań, domów i biur. Malowanie, układanie płytek, gładzie, zabudowy GK i adaptacje poddaszy.',
icon: 'PaintBucket',
href: '/uslugi-domowe/remonty'
},
{
title: 'Usługi hydrauliczne',
description: 'Biały montaż, naprawa wycieków, instalacje wod-kan. Pogotowie hydrauliczne 24/7 dla naszych stałych klientów.',
icon: 'Wrench',
href: '/uslugi-domowe/hydraulika'
},
{
title: 'Serwis AGD / Lodówki',
description: 'Szybka i skuteczna naprawa lodówek, chłodziarek i zamrażarek w Twoim domu. Diagnoza usterki od ręki.',
icon: 'Refrigerator',
href: '/uslugi-domowe/serwis-agd'
},
{
title: 'Drobne naprawy',
description: 'Usługi "Złotej Rączki". Pomożemy w montażu mebli, wieszaniu telewizorów, rolet i innych drobnych pracach domowych.',
icon: 'Hammer',
href: '/kontakt'
}
];
export default function UslugiDomowePage() {
return (
<main>
<HeroSection
title="Usługi domowe — remonty, hydraulika i naprawy w jednym miejscu"
subtitle="Szukasz solidnej ekipy wykończeniowej lub doświadczonego hydraulika? Oferujemy najwyższą jakość prac instalacyjno-remontowych podpartą gwarancją."
ctaText="Darmowa konsultacja"
ctaLink="/kontakt"
backgroundImage="https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=1600&q=80"
/>
<ServicesGrid
services={domoweServices}
eyebrow="Zakres Prac"
title="W czym możemy Ci pomóc?"
subtitle="Wybierz interesującą Cię usługę, aby poznać szczegóły, cennik oraz nasze dotychczasowe realizacje."
/>
<ProcessSteps
steps={homeProcessSteps}
eyebrow="Współpraca"
title="Jak pracujemy?"
subtitle="Przejrzyste zasady i terminowość to fundamenty naszej pracy."
/>
<WhyUs />
<ProjectsGallery
category="remonty"
title="Nasze realizacje"
description="Przejrzyj galerię naszych prac wykończeniowych i instalacyjnych."
/>
<CtaBanner
title="Potrzebujesz pomocy fachowca?"
subtitle="Zadzwoń do nas lub napisz. Odpowiadamy najszybciej jak to możliwe."
ctaText="Skontaktuj się z nami"
ctaLink="/kontakt"
/>
</main>
);
}
+94
View File
@@ -0,0 +1,94 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { ServicesGrid } from '@/components/sections/ServicesGrid';
import { ProcessSteps } from '@/components/sections/ProcessSteps';
import { BeforeAfterSlider } from '@/components/sections/BeforeAfterSlider';
import { WhyUs } from '@/components/sections/WhyUs';
import { Testimonials } from '@/components/sections/Testimonials';
import { FaqAccordion } from '@/components/sections/FaqAccordion';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { homeProcessSteps } from '@/content/process';
export const metadata: Metadata = {
title: 'Remonty i Wykończenia Wnętrz | AirProject',
description: 'Kompleksowe remonty i wykończenia wnętrz pod klucz. Gładzie, malowanie, układanie płytek, zabudowy GK. Własna, sprawdzona ekipa i gwarancja terminowości.',
alternates: {
canonical: '/uslugi-domowe/remonty',
}
};
const remontyServices = [
{
title: 'Wykończenia pod klucz',
description: 'Realizujemy pełne adaptacje mieszkań deweloperskich, domów oraz przestrzeni biurowych od stanu surowego do wprowadzenia.',
icon: 'Key',
},
{
title: 'Remonty łazienek i kuchni',
description: 'Najtrudniejsze pomieszczenia to nasza specjalność. Układamy glazurę, instalujemy biały montaż i przerabiamy instalacje.',
icon: 'Bath',
},
{
title: 'Malowanie i gładzie',
description: 'Szpachlowanie natryskowe, gładzie gipsowe oraz perfekcyjne malowanie farbami tradycyjnymi i dekoracyjnymi.',
icon: 'PaintRoller',
},
{
title: 'Zabudowy G-K',
description: 'Wykonujemy sufity podwieszane, ściany działowe, adaptacje poddaszy i nietypowe zabudowy z płyt kartonowo-gipsowych.',
icon: 'Grid',
},
{
title: 'Układanie podłóg',
description: 'Montaż paneli podłogowych, deski barlineckiej, wykładzin oraz listew przypodłogowych.',
icon: 'Layers',
},
{
title: 'Prace wyburzeniowe',
description: 'Bezpieczne wyburzanie ścian działowych, skuwanie starych posadzek i tynków wraz z wywozem gruzu.',
icon: 'Hammer',
}
];
export default function RemontyPage() {
return (
<main>
<HeroSection
title="Remonty i wykończenia wnętrz"
subtitle="Zmieniamy puste przestrzenie w wymarzone domy, a stare mieszkania poddajemy całkowitej metamorfozie. Solidnie, czysto i na czas."
ctaText="Bezpłatna wycena"
ctaLink="/kontakt"
backgroundImage="https://images.unsplash.com/photo-1505691938895-1758d7feb511?w=1600&q=80"
/>
<ServicesGrid
services={remontyServices}
eyebrow="Zakres Prac"
title="Co możemy dla Ciebie zrobić?"
subtitle="Wykonujemy praktycznie każdy rodzaj prac remontowo-budowlanych wewnątrz budynków."
/>
<ProcessSteps
steps={homeProcessSteps}
eyebrow="Współpraca"
title="Proces realizacji remontu"
subtitle="Remont nie musi być stresujący. Dzięki naszemu doświadczeniu przeprowadzimy Cię przez niego gładko."
/>
<BeforeAfterSlider />
<WhyUs />
<Testimonials />
<FaqAccordion category="remonty" />
<CtaBanner
title="Masz w planach remont?"
subtitle="Umów się na niezobowiązujące oględziny. Przygotujemy dla Ciebie darmowy kosztorys."
ctaText="Zamów wycenę"
ctaLink="/kontakt"
/>
</main>
);
}
+90
View File
@@ -0,0 +1,90 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { ServicesGrid } from '@/components/sections/ServicesGrid';
import { SymptomsList } from '@/components/sections/SymptomsList';
import { ProcessSteps } from '@/components/sections/ProcessSteps';
import { WhyUs } from '@/components/sections/WhyUs';
import { FaqAccordion } from '@/components/sections/FaqAccordion';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { homeProcessSteps } from '@/content/process';
export const metadata: Metadata = {
title: 'Serwis AGD, Naprawa Lodówek i Zamrażarek | AirProject',
description: 'Mobilny serwis sprzętu chłodniczego. Dojeżdżamy do klienta i naprawiamy większość usterek na miejscu. Oryginalne części i gwarancja na naprawę.',
alternates: {
canonical: '/uslugi-domowe/serwis-agd',
}
};
const agdServices = [
{
title: 'Lodówki i chłodziarki',
description: 'Naprawiamy wolnostojące i zabudowane lodówki wszystkich wiodących marek (Samsung, LG, Beko, Bosch, Indesit i inne).',
icon: 'Refrigerator',
},
{
title: 'Zamrażarki',
description: 'Serwis zamrażarek skrzyniowych i szufladowych. Uzupełnianie czynnika chłodniczego, wymiana termostatów.',
icon: 'Snowflake',
},
{
title: 'Urządzenia No Frost',
description: 'Znamy specyfikę nowoczesnych systemów bezszronowych. Diagnozujemy problemy z grzałkami, czujnikami i wentylatorami.',
icon: 'Wind',
},
{
title: 'Lady i witryny chłodnicze',
description: 'Oferujemy wsparcie również dla mniejszych firm z branży gastro i handlu. Szybka reakcja, aby uratować towar.',
icon: 'Store',
},
{
title: 'Wymiana uszczelek',
description: 'Dopasowanie i montaż nowych uszczelek drzwiowych, co pozwala obniżyć rachunki za prąd i zredukować oszranianie.',
icon: 'ScanLine',
},
{
title: 'Wymiana sprężarek',
description: 'Nawet najpoważniejsze usterki agregatów nie są dla nas problemem. Wymieniamy kompresory i lutujemy układ.',
icon: 'Settings',
}
];
export default function SerwisAgdPage() {
return (
<main>
<HeroSection
title="Serwis lodówek i urządzeń chłodniczych"
subtitle="Twoja lodówka przestała chłodzić? Wydaje dziwne dźwięki? Naprawiamy sprzęt chłodniczy bezpośrednio w domu klienta."
ctaText="Zgłoś naprawę"
ctaLink="/kontakt"
backgroundImage="https://images.unsplash.com/photo-1584269600464-37b1b58a9fe7?w=1600&q=80"
/>
<SymptomsList />
<ServicesGrid
services={agdServices}
eyebrow="Co naprawiamy?"
title="Naprawimy każdy sprzęt chłodniczy"
subtitle="Nie musisz martwić się o transport gabarytów. W 90% przypadków usterkę da się usunąć na miejscu."
/>
<ProcessSteps
steps={homeProcessSteps}
eyebrow="Przebieg naprawy"
title="Jak wygląda wezwanie serwisu?"
/>
<WhyUs />
<FaqAccordion category="serwis-agd" />
<CtaBanner
title="Masz problem ze swoim sprzętem?"
subtitle="Podaj markę i objawy usterki, a postaramy się wycenić naprawę już przez telefon."
ctaText="Napisz do nas"
ctaLink="/kontakt"
/>
</main>
);
}
+38
View File
@@ -0,0 +1,38 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { ContactSection } from '@/components/sections/ContactSection';
export const metadata: Metadata = {
title: 'Zamówienie | AirProject',
description: 'Dokończ swoje zamówienie na urządzenia i montaż, zostawiając nam swoje dane.',
alternates: {
canonical: '/zamowienie',
}
};
export default function CheckoutPage() {
return (
<main className="bg-slate-50 min-h-screen">
<HeroSection
eyebrow="Sklep"
title="Dokończ zamówienie"
subtitle="Zgodnie z wymogami prawnymi (F-Gazy) oraz chęcią zapewnienia najwyższej jakości obsługi, wszystkie zamówienia finalizujemy po krótkim kontakcie. Zostaw swoje dane, a my zajmiemy się resztą!"
ctaText="Wróć do koszyka"
ctaLink="/koszyk"
backgroundImage="https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=1600&q=80"
/>
<div className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-12 text-center max-w-3xl mx-auto">
<h2 className="text-3xl font-extrabold text-primary font-heading mb-4">Wypełnij formularz</h2>
<p className="text-text-muted text-lg">
Nasz doradca skontaktuje się z Tobą w ciągu maksymalnie 24 godzin, aby potwierdzić
dostępność urządzeń z Twojego koszyka, omówić szczegóły montażu oraz zorganizować wysyłkę.
</p>
</div>
<ContactSection />
</div>
</main>
);
}
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
+79
View File
@@ -0,0 +1,79 @@
import Link from "next/link";
import {
Wind, Wrench, ThermometerSun, Paintbrush, Droplet, Tv, HelpCircle, ArrowRight,
PaintBucket, Refrigerator, Hammer, Key, Bath, PaintRoller, Grid, Layers,
AlertTriangle, Waves, Droplets, WashingMachine, Snowflake, Store, ScanLine, Settings
} from "lucide-react";
import { cn } from "@/lib/utils";
interface IconCardProps {
title: string;
description: string;
iconName: string;
href?: string;
className?: string;
}
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
Wind,
Wrench,
ThermometerSun,
Paintbrush,
Droplet,
Tv,
PaintBucket,
Refrigerator,
Hammer,
Key,
Bath,
PaintRoller,
Grid,
Layers,
AlertTriangle,
Waves,
Droplets,
WashingMachine,
Snowflake,
Store,
ScanLine,
Settings,
};
export function IconCard({ title, description, iconName, href, className }: IconCardProps) {
const Icon = iconMap[iconName] || HelpCircle;
const cardContent = (
<>
<div className="p-3 rounded-lg bg-slate-100 text-primary w-fit group-hover:bg-accent-light group-hover:text-accent transition-colors duration-300">
<Icon className="size-6" />
</div>
<h3 className="text-lg font-semibold text-primary font-heading mt-4 group-hover:text-accent transition-colors">
{title}
</h3>
<p className="text-sm text-text-muted mt-2 leading-relaxed flex-grow">
{description}
</p>
{href && (
<div className="flex items-center gap-1 text-xs font-semibold text-primary group-hover:text-accent mt-4 transition-colors">
<span>Dowiedz się więcej</span>
<ArrowRight className="size-3.5 group-hover:translate-x-1 transition-transform" />
</div>
)}
</>
);
const cardClasses = cn(
"group border border-border rounded-xl p-6 bg-white hover:shadow-md transition-all duration-300 flex flex-col justify-between h-full",
className
);
if (href) {
return (
<Link href={href} className={cardClasses}>
{cardContent}
</Link>
);
}
return <div className={cardClasses}>{cardContent}</div>;
}
+35
View File
@@ -0,0 +1,35 @@
import { Image as ImageIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface PlaceholderProps {
text: string;
className?: string;
aspect?: "video" | "square" | "portrait" | "auto" | "banner";
}
export function Placeholder({ text, className, aspect = "video" }: PlaceholderProps) {
const aspectClasses = {
video: "aspect-video",
square: "aspect-square",
portrait: "aspect-[3/4]",
banner: "aspect-[21/9] min-h-[300px]",
auto: "h-full w-full",
};
return (
<div
className={cn(
"relative w-full bg-slate-100 dark:bg-slate-800 border-2 border-dashed border-slate-200 dark:border-slate-700 rounded-xl flex flex-col items-center justify-center text-center p-6 text-slate-400 dark:text-slate-500 overflow-hidden group hover:bg-slate-150 transition-colors duration-250",
aspectClasses[aspect],
className
)}
>
{/* TODO: docelowe zdjęcie — {text} */}
<ImageIcon className="size-10 mb-3 group-hover:scale-110 transition-transform duration-300" />
<span className="text-sm font-medium px-4 leading-normal">{text}</span>
<span className="text-[10px] text-slate-400 mt-2 uppercase tracking-widest font-semibold font-heading opacity-70">
Placeholder Zdjęcia
</span>
</div>
);
}
+58
View File
@@ -0,0 +1,58 @@
import { cn } from "@/lib/utils";
interface SectionHeadingProps {
title: string;
subtitle?: string;
eyebrow?: string;
align?: "left" | "center";
dark?: boolean;
className?: string;
}
export function SectionHeading({
title,
subtitle,
eyebrow,
align = "center",
dark = false,
className,
}: SectionHeadingProps) {
return (
<div
className={cn(
"flex flex-col gap-2 mb-10 md:mb-14",
align === "center" ? "items-center text-center" : "items-start text-left",
className
)}
>
{eyebrow && (
<span
className={cn(
"text-xs md:text-sm font-semibold uppercase tracking-wider font-heading",
dark ? "text-accent" : "text-cta"
)}
>
{eyebrow}
</span>
)}
<h2
className={cn(
"text-3xl md:text-4xl font-bold leading-tight font-heading max-w-3xl",
dark ? "text-white" : "text-primary"
)}
>
{title}
</h2>
{subtitle && (
<p
className={cn(
"text-base md:text-lg max-w-2xl leading-relaxed mt-1",
dark ? "text-slate-300" : "text-text-muted"
)}
>
{subtitle}
</p>
)}
</div>
);
}
+196
View File
@@ -0,0 +1,196 @@
"use client";
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
import { submitContactForm } from '@/app/kontakt/actions';
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.string().email("Niepoprawny adres e-mail"),
subject: z.string().min(1, "Wybierz temat"),
message: z.string().min(10, "Wiadomość musi mieć min. 10 znaków"),
consent: z.boolean().refine((val) => val === true, "Zgoda RODO jest wymagana"),
});
type ContactFormValues = z.infer<typeof contactFormSchema>;
export function ContactForm() {
const [serverState, setServerState] = useState<{ success?: boolean; message?: string } | null>(null);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset
} = useForm<ContactFormValues>({
resolver: zodResolver(contactFormSchema),
defaultValues: {
name: '',
phone: '',
email: '',
subject: '',
message: '',
consent: false,
}
});
const onSubmit = async (data: ContactFormValues) => {
setServerState(null);
// Przekształcamy dane do formatu FormData dla Server Action
const formData = new FormData();
Object.entries(data).forEach(([key, value]) => {
formData.append(key, value.toString());
});
const response = await submitContactForm(null, formData);
setServerState({
success: response.success,
message: response.message
});
if (response.success) {
reset();
}
};
if (serverState?.success) {
return (
<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">Wiadomość wysłana!</h3>
<p className="text-green-700">{serverState.message}</p>
<button
onClick={() => setServerState(null)}
className="mt-6 px-6 py-2 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700 transition-colors"
>
Wyślij kolejną wiadomość
</button>
</div>
);
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 bg-white p-6 md:p-8 rounded-2xl border border-border shadow-sm">
{serverState && !serverState.success && (
<div className="bg-red-50 text-red-800 p-4 rounded-xl border border-red-200 flex items-start gap-3">
<AlertCircle className="w-5 h-5 shrink-0 mt-0.5" />
<p className="text-sm font-medium">{serverState.message}</p>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Imię */}
<div className="space-y-2">
<label htmlFor="name" className="text-sm font-semibold text-primary">Imię i nazwisko</label>
<input
{...register("name")}
id="name"
className={`w-full px-4 py-3 rounded-xl border ${errors.name ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50`}
placeholder="Jan Kowalski"
aria-invalid={errors.name ? "true" : "false"}
/>
{errors.name && <p className="text-red-500 text-xs font-medium">{errors.name.message}</p>}
</div>
{/* Telefon */}
<div className="space-y-2">
<label htmlFor="phone" className="text-sm font-semibold text-primary">Telefon</label>
<input
{...register("phone")}
id="phone"
type="tel"
className={`w-full px-4 py-3 rounded-xl border ${errors.phone ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50`}
placeholder="+48 123 456 789"
/>
{errors.phone && <p className="text-red-500 text-xs font-medium">{errors.phone.message}</p>}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Email */}
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-semibold text-primary">Adres e-mail</label>
<input
{...register("email")}
id="email"
type="email"
className={`w-full px-4 py-3 rounded-xl border ${errors.email ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50`}
placeholder="[email protected]"
/>
{errors.email && <p className="text-red-500 text-xs font-medium">{errors.email.message}</p>}
</div>
{/* Temat */}
<div className="space-y-2">
<label htmlFor="subject" className="text-sm font-semibold text-primary">Temat rozmowy</label>
<select
{...register("subject")}
id="subject"
className={`w-full px-4 py-3 rounded-xl border ${errors.subject ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 appearance-none`}
>
<option value="">Wybierz temat...</option>
<option value="klimatyzacja">Klimatyzacja</option>
<option value="pompy-ciepla">Pompy ciepła</option>
<option value="remonty">Remonty i wykończenia</option>
<option value="hydraulika">Hydraulika</option>
<option value="serwis-agd">Serwis AGD</option>
<option value="inne">Inne zapytanie</option>
</select>
{errors.subject && <p className="text-red-500 text-xs font-medium">{errors.subject.message}</p>}
</div>
</div>
{/* Wiadomość */}
<div className="space-y-2">
<label htmlFor="message" className="text-sm font-semibold text-primary">Wiadomość</label>
<textarea
{...register("message")}
id="message"
rows={5}
className={`w-full px-4 py-3 rounded-xl border ${errors.message ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 resize-y`}
placeholder="Opisz krótko, w czym możemy Ci pomóc..."
/>
{errors.message && <p className="text-red-500 text-xs font-medium">{errors.message.message}</p>}
</div>
{/* RODO */}
<div className="space-y-2">
<label className="flex items-start gap-3 cursor-pointer group">
<div className="relative flex items-start pt-1">
<input
type="checkbox"
{...register("consent")}
className="w-5 h-5 rounded border-border text-primary focus:ring-primary focus:ring-offset-0 transition-colors"
/>
</div>
<span className="text-xs text-text-muted leading-relaxed select-none">
Wyrażam zgodę na przetwarzanie moich danych osobowych podanych w formularzu w celu obsługi zapytania przez firmę AeroService. Zapoznałem/am się z Polityką Prywatności.
</span>
</label>
{errors.consent && <p className="text-red-500 text-xs font-medium">{errors.consent.message}</p>}
</div>
{/* Button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-cta hover:bg-cta-hover text-white font-bold py-4 rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed shadow-md"
>
{isSubmitting ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>Wysyłanie...</span>
</>
) : (
<span>Wyślij zapytanie</span>
)}
</button>
</form>
);
}
+205
View File
@@ -0,0 +1,205 @@
import Link from "next/link";
import { Phone, Mail, MapPin, Clock, Facebook, Instagram, ShieldCheck } from "lucide-react";
import { companyDetails } from "@/content/company";
export function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-primary text-primary-foreground">
{/* Main Footer Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
{/* Column 1: Company details */}
<div className="flex flex-col gap-4">
<Link href="/" className="inline-block">
<span className="text-2xl font-bold font-heading text-white">
{companyDetails.shortName}
<span className="text-accent">.</span>
</span>
</Link>
<p className="text-sm text-slate-300 max-w-xs leading-relaxed">
Kompleksowe usługi HVAC, klimatyzacji, pomp ciepła oraz prace wykończeniowe i serwisowe dla Twojego domu.
</p>
<div className="flex flex-col gap-3 mt-2 text-sm text-slate-300">
<a
href={`tel:${companyDetails.phoneRaw}`}
className="flex items-center gap-2.5 hover:text-accent transition-colors"
>
<Phone className="size-4 shrink-0 text-accent" />
<span>{companyDetails.phone}</span>
</a>
<a
href={`mailto:${companyDetails.email}`}
className="flex items-center gap-2.5 hover:text-accent transition-colors"
>
<Mail className="size-4 shrink-0 text-accent" />
<span>{companyDetails.email}</span>
</a>
<div className="flex items-start gap-2.5">
<MapPin className="size-4 shrink-0 mt-0.5 text-accent" />
<span>
{companyDetails.address.street},<br />
{companyDetails.address.zipCode} {companyDetails.address.city}
</span>
</div>
<div className="flex items-start gap-2.5">
<Clock className="size-4 shrink-0 mt-0.5 text-accent" />
<span>{companyDetails.hours}</span>
</div>
<div className="text-xs text-slate-400 mt-1">
NIP: {companyDetails.nip}
</div>
</div>
</div>
{/* Column 2: Services */}
<div>
<h3 className="text-base font-semibold font-heading text-white mb-6 uppercase tracking-wider">
Usługi
</h3>
<ul className="flex flex-col gap-3 text-sm text-slate-300">
<li>
<Link href="/klimatyzacja" className="hover:text-accent transition-colors">
Klimatyzacja i Montaż
</Link>
</li>
<li>
<Link href="/pompy-ciepla" className="hover:text-accent transition-colors">
Pompy Ciepła
</Link>
</li>
<li>
<Link href="/uslugi-domowe/remonty" className="hover:text-accent transition-colors">
Remonty i Wykończenia
</Link>
</li>
<li>
<Link href="/uslugi-domowe/hydraulika" className="hover:text-accent transition-colors">
Usługi Hydrauliczne
</Link>
</li>
<li>
<Link href="/uslugi-domowe/serwis-agd" className="hover:text-accent transition-colors">
Serwis AGD i Lodówek
</Link>
</li>
<li>
<Link href="/sklep" className="hover:text-accent transition-colors">
Sklep online
</Link>
</li>
</ul>
</div>
{/* Column 3: Information */}
<div>
<h3 className="text-base font-semibold font-heading text-white mb-6 uppercase tracking-wider">
Informacje
</h3>
<ul className="flex flex-col gap-3 text-sm text-slate-300">
<li>
<Link href="/o-nas" className="hover:text-accent transition-colors">
O nas
</Link>
</li>
<li>
<Link href="/realizacje" className="hover:text-accent transition-colors">
Realizacje (Portfolio)
</Link>
</li>
<li>
<Link href="/blog" className="hover:text-accent transition-colors">
Blog i Poradniki
</Link>
</li>
<li>
<Link href="/regulamin" className="hover:text-accent transition-colors">
Regulamin
</Link>
</li>
<li>
<Link href="/polityka-prywatnosci" className="hover:text-accent transition-colors">
Polityka prywatności
</Link>
</li>
<li>
<Link href="/reklamacje" className="hover:text-accent transition-colors">
Reklamacje
</Link>
</li>
<li>
<Link href="/kontakt" className="hover:text-accent transition-colors">
Kontakt
</Link>
</li>
</ul>
</div>
{/* Column 4: Account */}
<div>
<h3 className="text-base font-semibold font-heading text-white mb-6 uppercase tracking-wider">
Twoje Konto
</h3>
<ul className="flex flex-col gap-3 text-sm text-slate-300">
<li>
<Link href="/koszyk" className="hover:text-accent transition-colors">
Twój Koszyk
</Link>
</li>
<li>
<Link href="/sklep" className="hover:text-accent transition-colors">
Zamów urządzenia
</Link>
</li>
<li>
<Link href="/kontakt" className="hover:text-accent transition-colors">
Zgłoś awarię / serwis
</Link>
</li>
</ul>
</div>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-slate-800 bg-slate-950/40 py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col sm:flex-row justify-between items-center gap-4">
<p className="text-xs text-slate-400 text-center sm:text-left">
© {currentYear} {companyDetails.name}. Wszelkie prawa zastrzeżone. Projekt strony i wdrożenie AeroService.
</p>
<div className="flex items-center gap-6">
{/* Social Icons */}
<div className="flex gap-4">
<a
href={companyDetails.socials.facebook}
target="_blank"
rel="noopener noreferrer"
className="text-slate-400 hover:text-white transition-colors"
aria-label="Odwiedź nasz profil na Facebooku"
>
<Facebook className="size-4" />
</a>
<a
href={companyDetails.socials.instagram}
target="_blank"
rel="noopener noreferrer"
className="text-slate-400 hover:text-white transition-colors"
aria-label="Odwiedź nasz profil na Instagramie"
>
<Instagram className="size-4" />
</a>
</div>
{/* Trust Symbol */}
<div className="flex items-center gap-1.5 text-xs text-slate-400 border-l border-slate-800 pl-6">
<ShieldCheck className="size-4 text-accent" />
<span>Bezpieczne płatności SSL</span>
</div>
</div>
</div>
</div>
</footer>
);
}
+94
View File
@@ -0,0 +1,94 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { Search } from "lucide-react";
import { getProducts } from "@/content/products";
import Link from "next/link";
import Image from "next/image";
import { formatCurrency } from "@/lib/utils";
export function GlobalSearch() {
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
const products = getProducts();
const results = query.length > 1
? products.filter(p => p.name.toLowerCase().includes(query.toLowerCase()) || p.brand.toLowerCase().includes(query.toLowerCase()) || p.category.toLowerCase().includes(query.toLowerCase()))
: [];
return (
<>
<button
onClick={() => setIsOpen(true)}
className="p-2 text-primary hover:text-accent hover:bg-background rounded-lg transition-colors outline-none cursor-pointer"
aria-label="Szukaj na stronie"
>
<Search className="size-5" />
</button>
<dialog className={`modal ${isOpen ? "modal-open" : ""}`}>
<div className="modal-box p-0 max-w-2xl bg-white rounded-2xl overflow-hidden mt-[-20vh]">
<div className="flex items-center border-b border-border px-4 py-3 bg-slate-50">
<Search className="size-5 text-accent mr-3 shrink-0" />
<input
autoFocus
type="text"
placeholder="Wpisz nazwę klimatyzatora (np. Rotenso, Pular)..."
className="flex-1 outline-none text-base placeholder:text-text-muted/50 bg-transparent py-2 text-primary font-medium"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button onClick={() => setIsOpen(false)} className="text-xs font-semibold text-text-muted hover:text-primary transition-colors bg-white px-2 py-1 rounded border border-border shadow-sm">
ESC
</button>
</div>
{query.length > 1 ? (
<div className="max-h-[60vh] overflow-y-auto p-2 bg-white">
{results.length > 0 ? (
<div className="flex flex-col gap-1">
{results.map((product) => (
<Link
key={product.slug}
href={`/sklep/${product.slug}`}
onClick={() => setIsOpen(false)}
className="flex items-center gap-4 p-3 hover:bg-slate-50 rounded-lg transition-colors group border border-transparent hover:border-border"
>
<div className="w-16 h-16 bg-white border border-border rounded-md overflow-hidden relative shrink-0">
<Image
src={product.image || 'https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=800&q=80'}
alt={product.name}
fill
className="object-contain p-1 mix-blend-multiply"
/>
</div>
<div className="flex-1 flex flex-col">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-wider">{product.brand}</span>
<span className="text-sm font-semibold text-primary group-hover:text-accent transition-colors">{product.name}</span>
<span className="text-xs text-text-muted mt-0.5">{product.power} kW • {product.category}</span>
</div>
<div className="text-right">
<span className="text-sm font-bold text-primary block">{formatCurrency(product.price)}</span>
</div>
</Link>
))}
</div>
) : (
<div className="p-8 text-center text-text-muted">
Brak wyników dla <span className="font-semibold">"{query}"</span>
</div>
)}
</div>
) : (
<div className="p-8 text-center text-text-muted/50 text-sm bg-white">
Wpisz co najmniej 2 znaki, aby rozpocząć wyszukiwanie w sklepie
</div>
)}
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={() => setIsOpen(false)}>close</button>
</form>
</dialog>
</>
);
}
+287
View File
@@ -0,0 +1,287 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { User, ShoppingBag, Menu, Phone } from "lucide-react";
import { NavDropdown } from "./NavDropdown";
import { GlobalSearch } from "./GlobalSearch";
import { MobileMenu } from "./MobileMenu";
import { useCartStore } from "@/lib/cart-store";
import { companyDetails } from "@/content/company";
import { cn } from "@/lib/utils";
export function Header() {
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const cartItems = useCartStore((state) => state.items);
useEffect(() => {
setMounted(true);
const handleScroll = () => {
if (window.scrollY > 40) {
setIsScrolled(true);
} else {
setIsScrolled(false);
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const cartCount = mounted
? cartItems.reduce((total, item) => total + item.quantity, 0)
: 0;
const acSections = [
{
title: "Klimatyzatory",
items: [
{
label: "Ścienne split",
href: "/sklep?category=scienne",
description: "Klasyczne jednostki ścienne do mieszkań",
},
{
label: "Multisplit",
href: "/sklep?category=multisplit",
description: "Systemy wielopokojowe (2-5 pomieszczeń)",
},
{
label: "Przenośne",
href: "/sklep?category=przenosne",
description: "Mobilne chłodzenie bez stałej instalacji",
},
{
label: "Kasetonowe i konsolowe",
href: "/sklep?category=kasetonowe",
description: "Modele biurowe i przypodłogowe",
},
],
},
{
title: "Usługi HVAC",
items: [
{
label: "Montaż klimatyzacji",
href: "/klimatyzacja",
description: "Kompleksowy dobór, audyt i montaż",
},
{
label: "Serwis i czyszczenie",
href: "/klimatyzacja",
description: "Odgrzybianie, naprawy i przeglądy",
},
],
},
];
const homeSections = [
{
title: "Prace wykończeniowe",
items: [
{
label: "Remonty i wykończenia",
href: "/uslugi-domowe/remonty",
description: "Kompleksowe remonty mieszkań i łazienek",
},
{
label: "Drobne naprawy",
href: "/uslugi-domowe",
description: "Pomoc „złotej rączki” w drobnych usterkach",
},
],
},
{
title: "Instalacje i AGD",
items: [
{
label: "Hydraulika",
href: "/uslugi-domowe/hydraulika",
description: "Montaż armatury, naprawy i instalacje wod-kan",
},
{
label: "Serwis AGD / Lodówki",
href: "/uslugi-domowe/serwis-agd",
description: "Szybka naprawa lodówek i urządzeń chłodniczych",
},
],
},
];
return (
<>
{/* ----------------- DESKTOP: TOP BAR ----------------- */}
<div className="hidden lg:block bg-muted py-2 border-b border-border/50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between">
{/* Top Bar Left: Logo */}
<Link href="/" className="shrink-0 flex items-center gap-3">
<span className="text-2xl font-bold font-heading text-primary">
{companyDetails.shortName}
<span className="text-accent">.</span>
</span>
<span className="text-xs text-muted-foreground font-medium border-l border-border pl-3">
Klimatyzacja i usługi domowe
</span>
</Link>
{/* Top Bar Right: Phone & Icons */}
<div className="flex items-center gap-6">
<a
href={`tel:${companyDetails.phoneRaw}`}
className="flex items-center gap-2 text-sm font-semibold text-primary hover:text-accent transition-colors whitespace-nowrap"
>
<Phone className="size-4 text-accent" />
<span>{companyDetails.phone}</span>
</a>
<div className="flex items-center gap-1">
<GlobalSearch />
<Link
href="/konto"
className="p-2 text-primary hover:text-accent hover:bg-background rounded-lg transition-colors outline-none"
aria-label="Konto użytkownika"
>
<User className="size-5" />
</Link>
<Link
href="/koszyk"
className="p-2 text-primary hover:text-accent hover:bg-background rounded-lg transition-colors relative outline-none"
aria-label={`Koszyk z ${cartCount} przedmiotami`}
>
<ShoppingBag className="size-5" />
{cartCount > 0 && (
<span className="absolute top-0 right-0 bg-cta text-white text-[10px] font-bold size-4 rounded-full flex items-center justify-center translate-x-0.5 -translate-y-0.5">
{cartCount}
</span>
)}
</Link>
</div>
</div>
</div>
</div>
{/* ----------------- DESKTOP: NAV BAR (STICKY) ----------------- */}
<header
className={cn(
"hidden lg:block sticky top-0 z-40 bg-background transition-all duration-300 border-b border-border",
isScrolled ? "shadow-md py-2" : "py-4"
)}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between">
{/* Main Navigation */}
<nav className="flex items-center gap-8">
{isScrolled && (
<Link href="/" className="shrink-0 flex items-center mr-2">
<span className="text-xl font-bold font-heading text-primary">
{companyDetails.shortName}
<span className="text-accent">.</span>
</span>
</Link>
)}
<NavDropdown
label="Klimatyzacja"
triggerHref="/klimatyzacja"
sections={acSections}
align="left"
/>
<Link
href="/pompy-ciepla"
className="text-sm font-medium text-primary hover:text-accent transition-colors"
>
Pompy ciepła
</Link>
<NavDropdown
label="Usługi domowe"
triggerHref="/uslugi-domowe"
sections={homeSections}
/>
<Link
href="/realizacje"
className="text-sm font-medium text-primary hover:text-accent transition-colors"
>
Realizacje
</Link>
<Link
href="/blog"
className="text-sm font-medium text-primary hover:text-accent transition-colors"
>
Blog
</Link>
<Link
href="/o-nas"
className="text-sm font-medium text-primary hover:text-accent transition-colors"
>
O nas
</Link>
<Link
href="/kontakt"
className="text-sm font-medium text-primary hover:text-accent transition-colors"
>
Kontakt
</Link>
</nav>
{/* CTA Button */}
<Link href="/kontakt" className="hidden lg:flex">
<button className="btn btn-primary font-bold">
Darmowa wycena
</button>
</Link>
</div>
</header>
{/* ----------------- MOBILE: HEADER (STICKY) ----------------- */}
<header
className={cn(
"lg:hidden sticky top-0 z-40 bg-background border-b border-border transition-all duration-300",
isScrolled ? "shadow-md py-3" : "py-4"
)}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 flex items-center justify-between">
{/* Mobile Logo */}
<Link href="/" className="shrink-0 flex items-center">
<span className="text-xl font-bold font-heading text-primary">
{companyDetails.shortName}
<span className="text-accent">.</span>
</span>
</Link>
{/* Mobile Icons (Cart + Hamburger) */}
<div className="flex items-center gap-3">
<Link
href="/koszyk"
className="p-2 text-primary hover:text-accent transition-colors relative outline-none"
aria-label={`Koszyk z ${cartCount} przedmiotami`}
>
<ShoppingBag className="size-6" />
{cartCount > 0 && (
<span className="absolute top-0 right-0 bg-cta text-white text-[10px] font-bold size-4 rounded-full flex items-center justify-center translate-x-0.5 -translate-y-0.5">
{cartCount}
</span>
)}
</Link>
<button
onClick={() => setIsMobileMenuOpen(true)}
className="p-2 text-primary hover:text-accent hover:bg-muted transition-colors rounded-lg outline-none"
aria-label="Otwórz menu"
>
<Menu className="size-6" />
</button>
</div>
</div>
</header>
{/* ----------------- MOBILE MENU DRAWER ----------------- */}
<MobileMenu
isOpen={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)}
/>
</>
);
}
+206
View File
@@ -0,0 +1,206 @@
"use client";
import { useEffect } from "react";
import Link from "next/link";
import { X, Phone, ChevronRight } from "lucide-react";
import { companyDetails } from "@/content/company";
import { cn } from "@/lib/utils";
interface MobileMenuProps {
isOpen: boolean;
onClose: () => void;
}
export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
// Prevent body scrolling when menu is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
const hvacSections = [
{ label: "Ofertowana klimatyzacja", href: "/klimatyzacja" },
{ label: "Klimatyzatory ścienne", href: "/sklep?category=scienne" },
{ label: "Klimatyzatory multisplit", href: "/sklep?category=multisplit" },
{ label: "Klimatyzatory przenośne", href: "/sklep?category=przenosne" },
{ label: "Klimatyzatory kasetonowe", href: "/sklep?category=kasetonowe" },
{ label: "Klimatyzator konsolowe", href: "/sklep?category=konsolowe" },
{ label: "Montaż klimatyzacji", href: "/klimatyzacja" },
{ label: "Serwis i przeglądy", href: "/klimatyzacja" },
];
const homeServices = [
{ label: "Wszystkie usługi domowe", href: "/uslugi-domowe" },
{ label: "Remonty i wykończenia", href: "/uslugi-domowe/remonty" },
{ label: "Hydraulika i instalacje", href: "/uslugi-domowe/hydraulika" },
{ label: "Serwis AGD / Lodówki", href: "/uslugi-domowe/serwis-agd" },
];
return (
<div
className={cn(
"fixed inset-0 z-50 lg:hidden transition-all duration-300",
isOpen ? "pointer-events-auto" : "pointer-events-none"
)}
>
{/* Dark Overlay */}
<div
className={cn(
"absolute inset-0 bg-black/60 transition-opacity duration-300",
isOpen ? "opacity-100" : "opacity-0"
)}
onClick={onClose}
/>
{/* Drawer Panel */}
<div
className={cn(
"absolute top-0 right-0 bottom-0 w-full max-w-sm bg-white shadow-xl flex flex-col transition-transform duration-300 ease-in-out",
isOpen ? "translate-x-0" : "translate-x-full"
)}
>
{/* Drawer Header */}
<div className="flex justify-between items-center px-6 py-4 border-b border-border">
<span className="text-xl font-bold font-heading text-primary">
Menu<span className="text-accent">.</span>
</span>
<button
onClick={onClose}
className="p-1 rounded-lg hover:bg-muted text-primary transition-colors focus:ring-2 focus:ring-accent outline-none"
aria-label="Zamknij menu"
>
<X className="size-6" />
</button>
</div>
{/* Drawer Content */}
<div className="flex-1 overflow-y-auto px-6 py-4">
<div className="w-full">
{/* 1. Klimatyzacja */}
<details className="collapse collapse-arrow border-b border-border rounded-none">
<summary className="collapse-title text-sm font-medium text-primary hover:text-accent py-3 px-0 min-h-0">
Klimatyzacja
</summary>
<div className="collapse-content px-0 pb-2">
<ul className="flex flex-col gap-2.5 pl-4 pb-2">
{hvacSections.map((item, idx) => (
<li key={idx}>
<Link
href={item.href}
onClick={onClose}
className="text-sm text-text-muted hover:text-accent transition-colors flex items-center justify-between py-1"
>
<span>{item.label}</span>
<ChevronRight className="size-3.5 opacity-60" />
</Link>
</li>
))}
</ul>
</div>
</details>
{/* 2. Pompy Ciepła */}
<div className="border-b border-border py-3">
<Link
href="/pompy-ciepla"
onClick={onClose}
className="text-sm font-medium text-primary hover:text-accent block"
>
Pompy ciepła
</Link>
</div>
{/* 3. Usługi Domowe */}
<details className="collapse collapse-arrow border-b border-border rounded-none">
<summary className="collapse-title text-sm font-medium text-primary hover:text-accent py-3 px-0 min-h-0">
Usługi domowe
</summary>
<div className="collapse-content px-0 pb-2">
<ul className="flex flex-col gap-2.5 pl-4 pb-2">
{homeServices.map((item, idx) => (
<li key={idx}>
<Link
href={item.href}
onClick={onClose}
className="text-sm text-text-muted hover:text-accent transition-colors flex items-center justify-between py-1"
>
<span>{item.label}</span>
<ChevronRight className="size-3.5 opacity-60" />
</Link>
</li>
))}
</ul>
</div>
</details>
{/* 4. Realizacje */}
<div className="border-b border-border py-3">
<Link
href="/realizacje"
onClick={onClose}
className="text-sm font-medium text-primary hover:text-accent block"
>
Realizacje
</Link>
</div>
{/* 5. Blog */}
<div className="border-b border-border py-3">
<Link
href="/blog"
onClick={onClose}
className="text-sm font-medium text-primary hover:text-accent block"
>
Blog
</Link>
</div>
{/* 6. O Nas */}
<div className="border-b border-border py-3">
<Link
href="/o-nas"
onClick={onClose}
className="text-sm font-medium text-primary hover:text-accent block"
>
O nas
</Link>
</div>
{/* 7. Kontakt */}
<div className="border-b border-border py-3">
<Link
href="/kontakt"
onClick={onClose}
className="text-sm font-medium text-primary hover:text-accent block"
>
Kontakt
</Link>
</div>
</div>
</div>
{/* Drawer Footer */}
<div className="border-t border-border p-6 bg-slate-50 flex flex-col gap-4">
<a
href={`tel:${companyDetails.phoneRaw}`}
className="flex items-center justify-center gap-2 p-3 bg-white border border-border rounded-xl text-primary font-semibold hover:text-accent transition-colors shadow-sm"
>
<Phone className="size-4 text-accent" />
<span>Zadzwoń: {companyDetails.phone}</span>
</a>
<Link href="/kontakt" onClick={onClose}>
<button className="btn btn-primary w-full font-bold">
Darmowa konsultacja
</button>
</Link>
</div>
</div>
</div>
);
}
+101
View File
@@ -0,0 +1,101 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
interface DropdownItem {
label: string;
href: string;
description?: string;
}
interface DropdownSection {
title?: string;
items: DropdownItem[];
}
interface NavDropdownProps {
label: string;
triggerHref?: string;
sections: DropdownSection[];
align?: "left" | "center" | "right";
}
export function NavDropdown({ label, triggerHref, sections, align = "center" }: NavDropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
setIsOpen(false);
}
};
return (
<div
className="relative group"
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
onKeyDown={handleKeyDown}
>
<div className="flex items-center gap-1 py-2 text-sm font-medium text-primary hover:text-accent cursor-pointer transition-colors focus-within:text-accent">
{triggerHref ? (
<Link href={triggerHref} className="outline-none">
{label}
</Link>
) : (
<span tabIndex={0} className="outline-none select-none">
{label}
</span>
)}
<ChevronDown className="size-4 transition-transform duration-200 group-hover:rotate-180" />
</div>
{/* Dropdown Panel */}
<div
className={cn(
"absolute top-full pt-2 z-50 w-[480px] transition-all duration-200 origin-top",
align === "left" && "left-0",
align === "center" && "left-1/2 -translate-x-1/2",
align === "right" && "right-0",
isOpen
? "opacity-100 translate-y-0 scale-100 pointer-events-auto"
: "opacity-0 -translate-y-2 scale-95 pointer-events-none"
)}
>
<div className="bg-white rounded-xl shadow-lg border border-border p-6 grid grid-cols-2 gap-8">
{sections.map((section, sIdx) => (
<div key={sIdx} className="flex flex-col gap-3">
{section.title && (
<h4 className="text-xs font-semibold uppercase tracking-wider text-text-muted mb-1 font-heading">
{section.title}
</h4>
)}
<ul className="flex flex-col gap-2">
{section.items.map((item, iIdx) => (
<li key={iIdx}>
<Link
href={item.href}
className="group/item flex flex-col gap-0.5 p-2 -mx-2 rounded-lg hover:bg-muted transition-colors"
onClick={() => setIsOpen(false)}
>
<span className="text-sm font-medium text-primary group-hover/item:text-accent transition-colors">
{item.label}
</span>
{item.description && (
<span className="text-xs text-text-muted leading-snug">
{item.description}
</span>
)}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
);
}
+56
View File
@@ -0,0 +1,56 @@
import { Clock, ShieldCheck, Shield, Percent, Star } from "lucide-react";
export function TrustBar() {
const items = [
{
icon: Clock,
title: "Szybki termin realizacji",
description: "Ekspresowy montaż i serwis",
},
{
icon: ShieldCheck,
title: "15+ lat doświadczenia",
description: "Tysiące zrealizowanych projektów",
},
{
icon: Shield,
title: "Pełna gwarancja",
description: "Bezpieczeństwo i pewność usług",
},
{
icon: Star,
title: "Opinie Google 5.0",
description: "Zaufanie setek klientów",
},
];
return (
<section className="bg-muted border-t border-b border-border py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 text-center lg:text-left">
{items.map((item, idx) => {
const Icon = item.icon;
return (
<div
key={idx}
className="flex flex-col lg:flex-row items-center lg:items-start gap-3 hover:translate-y-[-2px] transition-transform duration-200"
>
<div className="p-2 rounded-lg bg-accent-light text-accent shrink-0">
<Icon className="size-6" />
</div>
<div>
<h4 className="text-sm font-semibold text-primary font-heading">
{item.title}
</h4>
<p className="text-xs text-text-muted mt-0.5">
{item.description}
</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
}
+71
View File
@@ -0,0 +1,71 @@
import React from 'react';
import Image from 'next/image';
import { SectionHeading } from '../common/SectionHeading';
import { CheckCircle2 } from 'lucide-react';
import { companyDetails } from '@/content/company';
export function AboutStory() {
return (
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
{/* Lewa: Obraz */}
<div className="relative">
<div className="relative aspect-[4/5] rounded-3xl overflow-hidden shadow-2xl">
<Image
src="https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=800&q=80"
alt="Zespół firmy w trakcie pracy"
fill
className="object-cover"
/>
</div>
{/* Ozdobny element zaufania */}
<div className="absolute -bottom-8 -right-8 bg-white p-6 rounded-2xl shadow-xl hidden md:block">
<div className="flex items-center gap-4">
<div className="flex-shrink-0 w-16 h-16 bg-accent/10 rounded-full flex items-center justify-center">
<span className="text-2xl font-black text-accent font-heading">
{companyDetails.stats.find(s => s.label.includes("doświadczenia"))?.value || "15+"}
</span>
</div>
<div>
<p className="font-bold text-primary font-heading text-lg leading-none mb-1">Lat na rynku</p>
<p className="text-sm text-text-muted">Doświadczenie i zaufanie</p>
</div>
</div>
</div>
</div>
{/* Prawa: Tekst */}
<div>
<SectionHeading
eyebrow="Nasza Historia"
title="Budujemy komfort od fundamentów aż po dach"
align="left"
/>
<div className="prose prose-lg text-text-muted mt-8 mb-10">
<p>
Firma {companyDetails.name} powstała z pasji do nowoczesnych technologii oraz chęci dostarczania kompleksowych usług dla domów i firm. Zaczynaliśmy jako niewielka, dwuosobowa ekipa specjalizująca się w prostych remontach. Dziś jesteśmy liderem w regionie, realizując setki zaawansowanych projektów instalacyjnych rocznie.
</p>
<p>
Naszym celem jest upraszczanie życia naszym klientom. Zamiast szukać oddzielnych fachowców od hydrauliki, klimatyzacji i wykończeń, u nas otrzymujesz całościowe wsparcie z gwarancją jakości.
</p>
</div>
<ul className="space-y-4">
{[
"Certyfikowani instalatorzy z uprawnieniami F-Gaz",
"Dbałość o czystość podczas prac montażowych",
"Gwarancja i szybki serwis na terenie miasta i okolic",
"Pomoc w uzyskiwaniu dofinansowań"
].map((point, idx) => (
<li key={idx} className="flex items-start gap-3">
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
<span className="text-primary font-medium">{point}</span>
</li>
))}
</ul>
</div>
</div>
</section>
);
}
+78
View File
@@ -0,0 +1,78 @@
import React from 'react';
import Link from 'next/link';
import { Wind, LayoutGrid, Sparkles, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
const acTypes = [
{
title: 'Klimatyzatory SPLIT',
description: 'Najpopularniejsze rozwiązanie do jednego pomieszczenia. Składa się z jednej jednostki wewnętrznej i jednej zewnętrznej.',
icon: Wind,
subtypes: ['Ścienne', 'Konsolowe', 'Kasetonowe', 'Kanałowe'],
link: '/sklep/klimatyzatory-split',
},
{
title: 'Systemy MULTISPLIT',
description: 'Idealne do chłodzenia całego domu. Do jednej jednostki zewnętrznej można podłączyć od 2 do 5 jednostek wewnętrznych.',
icon: LayoutGrid,
subtypes: ['Dla 2 pomieszczeń', 'Dla 3 pomieszczeń', 'Dla 4-5 pomieszczeń', 'Mieszane typy jednostek'],
link: '/sklep/klimatyzatory-multisplit',
},
{
title: 'Pozostałe rozwiązania',
description: 'Specjalistyczne urządzenia łączące funkcje chłodzenia z zaawansowanym grzaniem lub oczyszczaniem powietrza.',
icon: Sparkles,
subtypes: ['Z funkcją ogrzewania (do -25°C)', 'Z zaawansowanym oczyszczaczem', 'Klimatyzatory przenośne', 'Klimatyzatory bez jednostki zewn.'],
link: '/sklep/inne',
},
];
export const AcTypesSection = () => {
return (
<section className="py-16 bg-gray-50">
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
<div className="text-center max-w-2xl mx-auto mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Rodzaje klimatyzatorów</h2>
<p className="text-gray-600">
Dobierzemy rozwiązanie idealnie dopasowane do Twoich potrzeb, niezależnie od tego czy chłodzisz jedno pomieszczenie, czy cały dom.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{acTypes.map((type, idx) => {
const Icon = type.icon;
return (
<div key={idx} className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 flex flex-col h-full hover:shadow-md transition-shadow">
<div className="w-14 h-14 bg-accent/10 rounded-xl flex items-center justify-center mb-6 text-accent">
<Icon className="w-7 h-7" />
</div>
<h3 className="text-xl font-bold text-primary mb-3">{type.title}</h3>
<p className="text-gray-600 mb-6 text-sm flex-grow">
{type.description}
</p>
<ul className="space-y-2 mb-8">
{type.subtypes.map((subtype, sIdx) => (
<li key={sIdx} className="flex items-center text-sm text-gray-700">
<div className="w-1.5 h-1.5 rounded-full bg-accent mr-3"></div>
{subtype}
</li>
))}
</ul>
<Link
href={type.link}
className={cn("btn btn-outline btn-primary bg-white w-full justify-between group mt-auto")}
>
Sprawdź modele
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
);
})}
</div>
</div>
</section>
);
};
+26
View File
@@ -0,0 +1,26 @@
import { CheckCircle2, Zap, ShieldCheck, Banknote } from "lucide-react";
export function AdvantagesBar() {
const advantages = [
{ icon: Zap, text: "Szybki termin realizacji" },
{ icon: ShieldCheck, text: "Gwarancja na sprzęt i montaż" },
{ icon: CheckCircle2, text: "Uprawnienia F-gaz" },
];
return (
<div className="bg-primary text-primary-foreground py-6 border-y border-border/10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6">
{advantages.map((adv, idx) => (
<div key={idx} className="flex items-center justify-center md:justify-start gap-3">
<adv.icon className="size-5 text-accent shrink-0" />
<span className="text-sm font-medium leading-tight">
{adv.text}
</span>
</div>
))}
</div>
</div>
</div>
);
}
+121
View File
@@ -0,0 +1,121 @@
'use client';
import React, { useState, useRef, useEffect, MouseEvent, TouchEvent } from 'react';
import Image from 'next/image';
import { projectsData } from '@/content/projects';
import { GripVertical } from 'lucide-react';
export const BeforeAfterSlider = () => {
const [sliderPosition, setSliderPosition] = useState(50);
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Pobierz pierwszy projekt z kategorii remonty, który ma beforeAfter
const project = projectsData.find(p => p.category === 'remonty' && p.beforeAfter);
if (!project || !project.beforeAfter) return null;
const handleMove = (clientX: number) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = Math.max(0, Math.min(clientX - rect.left, rect.width));
const percentage = (x / rect.width) * 100;
setSliderPosition(percentage);
};
const onMouseMove = (e: globalThis.MouseEvent) => {
if (!isDragging) return;
handleMove(e.clientX);
};
const onTouchMove = (e: globalThis.TouchEvent) => {
if (!isDragging) return;
handleMove(e.touches[0].clientX);
};
const stopDragging = () => {
setIsDragging(false);
};
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', stopDragging);
window.addEventListener('touchmove', onTouchMove);
window.addEventListener('touchend', stopDragging);
} else {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', stopDragging);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('touchend', stopDragging);
}
return () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', stopDragging);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('touchend', stopDragging);
};
}, [isDragging]);
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-5xl">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Przed i Po</h2>
<p className="text-gray-600">Zobacz metamorfozę na jednym z naszych ostatnich projektów.</p>
</div>
<div
ref={containerRef}
className="relative w-full aspect-[4/3] md:aspect-[16/9] rounded-2xl overflow-hidden select-none cursor-ew-resize bg-gray-100"
onMouseDown={() => setIsDragging(true)}
onTouchStart={() => setIsDragging(true)}
>
{/* Obrazek PO (na spodzie) */}
<div className="absolute inset-0">
<Image
src={project.beforeAfter.afterUrl}
alt="Wygląd po remoncie"
fill
className="object-cover pointer-events-none"
/>
<div className="absolute top-4 right-4 bg-white/80 backdrop-blur text-primary font-semibold px-4 py-1.5 rounded-full text-sm">
Po
</div>
</div>
{/* Obrazek PRZED (na wierzchu, obcięty) */}
<div
className="absolute inset-0 overflow-hidden"
style={{ width: `${sliderPosition}%` }}
>
<div className="absolute inset-0 w-[100vw] min-w-full">
{/* Używamy native img dla prostszego manipulowania wymiarami wewnątrz overflow */}
<img
src={project.beforeAfter.beforeUrl}
alt="Wygląd przed remontem"
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
style={{ width: containerRef.current?.getBoundingClientRect().width || '100%' }}
/>
</div>
<div className="absolute top-4 left-4 bg-white/80 backdrop-blur text-gray-700 font-semibold px-4 py-1.5 rounded-full text-sm">
Przed
</div>
</div>
{/* Suwak (linia i uchwyt) */}
<div
className="absolute top-0 bottom-0 w-1 bg-white cursor-ew-resize flex items-center justify-center shadow-[0_0_10px_rgba(0,0,0,0.3)]"
style={{ left: `calc(${sliderPosition}% - 2px)` }}
>
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center shadow-lg border border-gray-100 text-primary hover:scale-110 transition-transform">
<GripVertical className="w-5 h-5 text-gray-400" />
</div>
</div>
</div>
</div>
</section>
);
};
+57
View File
@@ -0,0 +1,57 @@
import React from 'react';
import { TrendingDown, Leaf, ThermometerSun, Wallet } from 'lucide-react';
const benefits = [
{
title: 'Znaczne oszczędności',
description: 'Nawet 70% niższe koszty ogrzewania w porównaniu do tradycyjnych źródeł ciepła, takich jak węgiel, olej czy gaz.',
icon: TrendingDown,
},
{
title: 'Ekologiczne ciepło',
description: 'Brak emisji spalin i zanieczyszczeń w miejscu działania. Pompa w 75% korzysta z odnawialnej energii z otoczenia.',
icon: Leaf,
},
{
title: 'Komfort całoroczny',
description: 'Bezobsługowy system zapewniający przyjemne ciepło zimą oraz chłodzenie i produkcję ciepłej wody użytkowej latem.',
icon: ThermometerSun,
},
{
title: 'Dotacje i ulgi',
description: 'Możliwość odliczenia od podatku oraz skorzystania z programów Czyste Powietrze lub Mój Prąd na pokrycie części kosztów.',
icon: Wallet,
},
];
export const BenefitsGrid = () => {
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
<div className="text-center max-w-2xl mx-auto mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Dlaczego warto wybrać pompę ciepła?</h2>
<p className="text-gray-600">
Pompa ciepła to obecnie najbardziej efektywne i przyszłościowe rozwiązanie do ogrzewania budynków.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{benefits.map((item, idx) => {
const Icon = item.icon;
return (
<div key={idx} className="bg-gray-50 border border-gray-100 rounded-2xl p-8 hover:shadow-lg transition-shadow">
<div className="w-14 h-14 bg-accent/10 rounded-2xl flex items-center justify-center mb-6 text-accent">
<Icon className="w-7 h-7" />
</div>
<h3 className="text-xl font-bold text-primary mb-3">{item.title}</h3>
<p className="text-gray-600 text-sm leading-relaxed">
{item.description}
</p>
</div>
);
})}
</div>
</div>
</section>
);
};
+107
View File
@@ -0,0 +1,107 @@
import Link from "next/link";
import { ArrowRight, Calendar, User } from "lucide-react";
import { SectionHeading } from "../common/SectionHeading";
import { Placeholder } from "../common/Placeholder";
import { getLatestPosts } from "@/content/posts";
export function BlogPreview() {
const latestPosts = getLatestPosts(3);
const formatDate = (dateStr: string) => {
const d = new Date(dateStr);
return d.toLocaleDateString("pl-PL", {
year: "numeric",
month: "long",
day: "numeric",
});
};
const getCategoryLabel = (cat: string) => {
const labels: Record<string, string> = {
klimatyzacja: "Klimatyzacja",
"pompy-ciepla": "Pompy ciepła",
remonty: "Remonty",
hydraulika: "Hydraulika",
porady: "Porady ogólne",
};
return labels[cat] || cat;
};
return (
<section className="py-16 md:py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Aktualności"
title="Najnowsze poradniki i artykuły"
subtitle="Dzielimy się wiedzą i doświadczeniem. Zobacz nasze wpisy dotyczące HVAC, remontów i oszczędzania energii."
align="center"
/>
{/* Blog Posts Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{latestPosts.map((post) => (
<article
key={post.slug}
className="group flex flex-col justify-between border border-border rounded-xl overflow-hidden hover:shadow-md transition-shadow h-full"
>
<div>
{/* Thumbnail */}
<div className="bg-slate-50 border-b border-border">
<Placeholder
text={`Miniatura artykułu: ${post.title}`}
aspect="video"
/>
</div>
{/* Content */}
<div className="p-6">
{/* Category & Date */}
<div className="flex items-center gap-4 text-xs text-text-muted mb-3">
<span className="font-semibold uppercase tracking-wider text-accent">
{getCategoryLabel(post.category)}
</span>
<span className="flex items-center gap-1">
<Calendar className="size-3.5" />
<span>{formatDate(post.date)}</span>
</span>
</div>
{/* Title */}
<h3 className="text-lg font-bold text-primary font-heading line-clamp-2 leading-snug group-hover:text-accent transition-colors">
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</h3>
{/* Excerpt */}
<p className="text-sm text-text-muted mt-3 line-clamp-2 leading-relaxed">
{post.excerpt}
</p>
</div>
</div>
{/* Read More Link */}
<div className="p-6 pt-0 mt-auto">
<Link
href={`/blog/${post.slug}`}
className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary group-hover:text-accent transition-colors"
>
<span>Czytaj więcej</span>
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</article>
))}
</div>
{/* Blog Catalog Button */}
<div className="flex justify-center mt-12">
<Link href="/blog">
<button className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 group">
<span>Przejdź do bloga</span>
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
</button>
</Link>
</div>
</div>
</section>
);
}
+34
View File
@@ -0,0 +1,34 @@
import React from 'react';
import Image from 'next/image';
const brands = [
{ name: 'Daikin', logo: '/brands/daikin.svg' }, // Placeholders, we will just show text if image fails or use a generic box
{ name: 'Mitsubishi', logo: '/brands/mitsubishi.svg' },
{ name: 'Gree', logo: '/brands/gree.svg' },
{ name: 'Rotenso', logo: '/brands/rotenso.svg' },
{ name: 'LG', logo: '/brands/lg.svg' },
{ name: 'Samsung', logo: '/brands/samsung.svg' },
{ name: 'Panasonic', logo: '/brands/panasonic.svg' },
{ name: 'Aux', logo: '/brands/aux.svg' },
];
export const BrandsGrid = () => {
return (
<section className="py-16 bg-white border-t border-gray-100">
<div className="container mx-auto px-4 md:px-8 max-w-7xl text-center">
<h3 className="text-xl font-semibold text-gray-500 mb-8 uppercase tracking-wider text-sm">
Autoryzowany partner czołowych producentów
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-6 items-center opacity-60 grayscale hover:grayscale-0 transition-all duration-500">
{brands.map((brand, idx) => (
<div key={idx} className="flex justify-center items-center h-16 bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-colors">
{/* Fallback to text for placeholder since we don't have images */}
<span className="font-bold text-gray-600 text-lg tracking-tight">{brand.name}</span>
</div>
))}
</div>
</div>
</section>
);
};
@@ -0,0 +1,37 @@
import React from 'react';
import Image from 'next/image';
import { SectionHeading } from '../common/SectionHeading';
import { companyDetails } from '@/content/company';
export function CertificatesSection() {
return (
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Uprawnienia"
title="Nasze certyfikaty"
subtitle="Zaufaj wykwalifikowanym specjalistom. Posiadamy wszystkie licencje wymagane do profesjonalnego montażu urządzeń HVAC."
align="center"
/>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-10 mt-16">
{companyDetails.certificates.map((cert, idx) => (
<div
key={idx}
className="group flex flex-col items-center text-center gap-4 p-6 bg-slate-50 rounded-2xl border border-border hover:border-accent/50 transition-colors"
>
{/* W rzeczywistości użylibyśmy obrazków. Tutaj dajemy szary prostokąt jako placeholder wg wytycznych */}
<div className="relative w-full aspect-[4/3] bg-white rounded-lg border border-border flex items-center justify-center p-4 overflow-hidden group-hover:shadow-md transition-shadow">
{/* Jeśli cert.image istnieje realnie, można by użyć Image, na razie tekstowy placeholder */}
<div className="text-sm font-bold text-slate-300 font-heading">
LOGO / CERTYFIKAT
</div>
</div>
<p className="font-semibold text-primary text-sm leading-tight group-hover:text-accent transition-colors">
{cert.name}
</p>
</div>
))}
</div>
</section>
);
}
@@ -0,0 +1,77 @@
import React from 'react';
import Link from 'next/link';
import { cn } from '@/lib/utils';
import { Settings2, Zap, Palette, Calculator, Lightbulb } from 'lucide-react';
const steps = [
{ icon: Settings2, label: 'Marka i model' },
{ icon: Zap, label: 'Moc chłodnicza' },
{ icon: Palette, label: 'Design i funkcje' },
{ icon: Calculator, label: 'Wycena montażu' },
];
export const ConfiguratorTeaser = () => {
return (
<section className="py-20 bg-primary text-white relative overflow-hidden">
{/* Background decoration */}
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-accent/20 to-transparent pointer-events-none" />
<div className="absolute -bottom-24 -left-24 w-64 h-64 bg-accent rounded-full blur-[120px] opacity-20 pointer-events-none" />
<div className="container mx-auto px-4 md:px-8 max-w-7xl relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-3xl md:text-4xl font-bold mb-6">
Skonfiguruj swój idealny zestaw w 4 krokach
</h2>
<p className="text-gray-300 text-lg mb-8 max-w-xl">
Nie wiesz jakie urządzenie wybrać? Nasz prosty konfigurator krok po kroku pomoże Ci dobrać sprzęt i natychmiast sprawdzić szacunkowy koszt wraz z montażem.
</p>
<div className="flex flex-col sm:flex-row gap-4 mb-10 lg:mb-0">
<Link
href="/kalkulator"
className={cn("btn btn-primary text-white group")}
>
Uruchom konfigurator
</Link>
</div>
</div>
<div className="relative">
{/* Visual representation of steps */}
<div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-2xl p-8">
<div className="space-y-6 relative before:absolute before:inset-0 before:ml-6 before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-white/30 before:to-transparent">
{steps.map((step, idx) => {
const Icon = step.icon;
return (
<div key={idx} className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group">
<div className="flex items-center justify-center w-12 h-12 rounded-full border-4 border-primary bg-accent text-white shadow md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2 z-10">
<Icon className="w-5 h-5" />
</div>
<div className="w-[calc(100%-4rem)] md:w-[calc(50%-3rem)] bg-white/10 border border-white/20 p-4 rounded-xl backdrop-blur-sm group-hover:bg-white/20 transition-colors">
<h4 className="font-semibold text-lg">{step.label}</h4>
<p className="text-sm text-gray-300">Krok {idx + 1}</p>
</div>
</div>
);
})}
</div>
{/* Pro tip card */}
<div className="mt-8 bg-accent/20 border border-accent/40 rounded-xl p-4 flex items-start">
<Lightbulb className="w-6 h-6 text-accent shrink-0 mr-3 mt-0.5" />
<div>
<h5 className="font-semibold text-accent-light mb-1">Porada eksperta</h5>
<p className="text-sm text-gray-200">
Szybki przelicznik mocy: <strong>10 m² = 1 kW</strong> mocy chłodniczej. Dla pokoju 25 m² szukaj urządzenia o mocy ok. 2.5 kW.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
+90
View File
@@ -0,0 +1,90 @@
import React from 'react';
import { companyDetails } from '@/content/company';
import { ContactForm } from '../forms/ContactForm';
import { MapPin, Phone, Mail, Clock, FileText } from 'lucide-react';
export function ContactSection() {
return (
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24">
{/* Lewa kolumna: Dane kontaktowe */}
<div className="flex flex-col">
<h2 className="text-3xl md:text-4xl font-extrabold text-primary font-heading mb-6">
Porozmawiajmy o Twoim projekcie
</h2>
<p className="text-text-muted text-lg mb-12">
Jesteśmy do Twojej dyspozycji. Zadzwoń, napisz e-mail lub skorzystaj z formularza obok. Odpowiadamy najszybciej jak to możliwe, najczęściej w ciągu kilku godzin.
</p>
<div className="space-y-8 flex-grow">
{/* Telefon */}
<div className="flex items-start gap-4 group">
<div className="w-12 h-12 rounded-xl bg-accent/10 text-accent flex items-center justify-center shrink-0 group-hover:bg-accent group-hover:text-white transition-colors">
<Phone className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Zadzwoń do nas</p>
<a href={`tel:${companyDetails.phoneRaw}`} className="text-2xl font-bold text-primary hover:text-accent transition-colors font-heading">
{companyDetails.phone}
</a>
</div>
</div>
{/* Email */}
<div className="flex items-start gap-4 group">
<div className="w-12 h-12 rounded-xl bg-accent/10 text-accent flex items-center justify-center shrink-0 group-hover:bg-accent group-hover:text-white transition-colors">
<Mail className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Napisz e-mail</p>
<a href={`mailto:${companyDetails.email}`} className="text-xl font-bold text-primary hover:text-accent transition-colors font-heading">
{companyDetails.email}
</a>
</div>
</div>
{/* Adres */}
<div className="flex items-start gap-4 group">
<div className="w-12 h-12 rounded-xl bg-accent/10 text-accent flex items-center justify-center shrink-0 group-hover:bg-accent group-hover:text-white transition-colors">
<MapPin className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Nasze biuro</p>
<div className="text-lg font-medium text-primary">
{companyDetails.name}<br />
{companyDetails.address.street}<br />
{companyDetails.address.zipCode} {companyDetails.address.city}
</div>
</div>
</div>
{/* Godziny i NIP */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8 pt-8 border-t border-border">
<div className="flex items-start gap-3">
<Clock className="w-5 h-5 text-primary/60 mt-0.5 shrink-0" />
<div>
<p className="text-sm font-semibold text-text-muted mb-1">Godziny pracy</p>
<p className="text-sm text-primary font-medium">{companyDetails.hours}</p>
</div>
</div>
<div className="flex items-start gap-3">
<FileText className="w-5 h-5 text-primary/60 mt-0.5 shrink-0" />
<div>
<p className="text-sm font-semibold text-text-muted mb-1">Dane firmy</p>
<p className="text-sm text-primary font-medium">NIP: {companyDetails.nip}</p>
</div>
</div>
</div>
</div>
</div>
{/* Prawa kolumna: Formularz */}
<div className="relative">
{/* Tło dekoracyjne */}
<div className="absolute inset-0 bg-primary/5 rounded-[2.5rem] transform translate-x-4 translate-y-4 -z-10 hidden sm:block" />
<ContactForm />
</div>
</div>
</section>
);
}
+91
View File
@@ -0,0 +1,91 @@
import Link from "next/link";
import { Phone, ArrowRight } from "lucide-react";
import { companyDetails } from "@/content/company";
import { cn } from "@/lib/utils";
interface CtaBannerProps {
title?: string;
subtitle?: string;
ctaText?: string;
ctaLink?: string;
bgVariant?: "primary" | "accent-light";
}
export function CtaBanner({
title = "Masz pytania? Umów darmową konsultację",
subtitle = "Skontaktuj się z nami! Doradzimy najlepsze rozwiązania, dobierzemy moc urządzeń i przygotujemy bezpłatną wycenę.",
ctaText = "Zamów bezpłatną konsultację",
ctaLink = "/kontakt",
bgVariant = "primary",
}: CtaBannerProps) {
const isPrimary = bgVariant === "primary";
return (
<section className="relative overflow-hidden py-16 md:py-20">
{/* Background and overlays */}
<div
className={cn(
"absolute inset-0 z-0",
isPrimary
? "bg-[#0F2A47] text-white"
: "bg-accent-light text-primary"
)}
/>
{isPrimary && (
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_left,rgba(43,168,224,0.12),transparent_40%)] z-0" />
)}
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col lg:flex-row items-center justify-between gap-10">
{/* Text Area */}
<div className="max-w-2xl text-center lg:text-left flex flex-col gap-3">
<h2
className={cn(
"text-2xl sm:text-3xl md:text-4xl font-extrabold font-heading leading-tight",
isPrimary ? "text-white" : "text-primary"
)}
>
{title}
</h2>
<p
className={cn(
"text-sm sm:text-base leading-relaxed max-w-xl",
isPrimary ? "text-slate-300" : "text-text-muted"
)}
>
{subtitle}
</p>
</div>
{/* Action buttons */}
<div className="flex flex-col sm:flex-row items-center gap-4 mt-8 w-full sm:w-auto shrink-0">
<Link href={ctaLink} className="w-full sm:w-auto">
<button className="btn btn-primary w-full sm:w-auto font-bold text-white">
<span>{ctaText}</span>
<ArrowRight className="size-5" />
</button>
</Link>
<a
href={`tel:${companyDetails.phoneRaw}`}
className="w-full sm:w-auto"
>
<button
className={cn(
"btn btn-outline btn-primary bg-white w-full sm:w-auto font-bold flex items-center gap-2",
isPrimary
? "text-white hover:bg-white/20 hover:text-white border-white/30"
: "text-primary hover:bg-primary/10 border-primary/20"
)}
>
<Phone className="size-5" />
<span>{companyDetails.phone}</span>
</button>
</a>
</div>
</div>
</div>
</section>
);
}
+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import { PhoneCall, Clock, ShieldAlert } from 'lucide-react';
import { companyDetails } from '@/content/company';
export const EmergencyBlock = () => {
return (
<section className="py-12 bg-cta text-white relative overflow-hidden">
{/* Dekoracyjne tło */}
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-white/10 to-transparent pointer-events-none" />
<div className="absolute -bottom-24 -left-24 w-64 h-64 bg-white/20 rounded-full blur-[100px] pointer-events-none" />
<div className="container mx-auto px-4 md:px-8 max-w-5xl relative z-10">
<div className="flex flex-col md:flex-row items-center justify-between gap-8">
<div className="flex-1 text-center md:text-left">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-white/20 rounded-full text-sm font-semibold mb-4 backdrop-blur-sm border border-white/30">
<ShieldAlert className="w-4 h-4" />
Pogotowie hydrauliczne
</div>
<h2 className="text-3xl md:text-4xl font-bold mb-4">
Masz awarię? Zadzwoń natychmiast!
</h2>
<p className="text-white/90 text-lg mb-0 max-w-xl mx-auto md:mx-0">
Pęknięta rura, zatkany odpływ lub wyciek? Zapewniamy szybką diagnozę i skuteczną naprawę usterki.
</p>
</div>
<div className="flex flex-col items-center sm:items-end gap-4 shrink-0">
<a
href={`tel:${companyDetails.phone.replace(/\s+/g, '')}`}
className="group flex items-center gap-4 bg-white text-primary px-8 py-4 rounded-xl font-bold text-2xl hover:bg-gray-50 transition-colors shadow-lg hover:shadow-xl"
>
<div className="w-12 h-12 bg-cta/10 rounded-full flex items-center justify-center text-cta group-hover:scale-110 transition-transform">
<PhoneCall className="w-6 h-6" />
</div>
{companyDetails.phone}
</a>
<div className="flex items-center gap-2 text-sm font-medium bg-black/10 px-4 py-2 rounded-lg">
<Clock className="w-4 h-4 text-white/80" />
<span className="text-white/90">Szybki czas reakcji na terenie całego miasta</span>
</div>
</div>
</div>
</div>
</section>
);
};
+71
View File
@@ -0,0 +1,71 @@
'use client';
import React, { useState } from 'react';
import { faqData, FaqItem } from '@/content/faq';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
interface FaqAccordionProps {
category: FaqItem['category'];
}
export const FaqAccordion = ({ category }: FaqAccordionProps) => {
const [openId, setOpenId] = useState<string | null>(null);
const faqs = faqData.filter(item => item.category === category);
if (faqs.length === 0) return null;
return (
<section className="py-16 bg-gray-50">
<div className="container mx-auto px-4 md:px-8 max-w-3xl">
<div className="text-center mb-10">
<h2 className="text-3xl font-bold text-primary mb-4">Często zadawane pytania</h2>
<p className="text-gray-600">
Odpowiedzi na najczęstsze wątpliwości naszych klientów.
</p>
</div>
<div className="space-y-4">
{faqs.map((faq) => {
const isOpen = openId === faq.id;
return (
<div
key={faq.id}
className={cn(
"bg-white border rounded-xl overflow-hidden transition-all duration-200",
isOpen ? "border-accent shadow-md" : "border-gray-200 hover:border-gray-300"
)}
>
<button
className="w-full px-6 py-4 flex items-center justify-between text-left focus:outline-none"
onClick={() => setOpenId(isOpen ? null : faq.id)}
aria-expanded={isOpen}
>
<span className="font-semibold text-primary pr-4">{faq.question}</span>
<ChevronDown
className={cn(
"w-5 h-5 text-gray-400 transition-transform duration-200 shrink-0",
isOpen && "rotate-180 text-accent"
)}
/>
</button>
<div
className={cn(
"px-6 text-gray-600 leading-relaxed overflow-hidden transition-all duration-300 ease-in-out",
isOpen ? "max-h-96 pb-5 opacity-100" : "max-h-0 pb-0 opacity-0"
)}
>
<div className="pt-2 border-t border-gray-100">
{faq.answer}
</div>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
+80
View File
@@ -0,0 +1,80 @@
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Post } from '@/types/types';
import { Calendar, User, ArrowRight } from 'lucide-react';
interface FeaturedPostProps {
post: Post;
}
export function FeaturedPost({ post }: FeaturedPostProps) {
// Formatowanie daty
const dateObj = new Date(post.date);
const formattedDate = dateObj.toLocaleDateString('pl-PL', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
// Tłumaczenie kategorii
const categoryLabels: Record<string, string> = {
'klimatyzacja': 'Klimatyzacja',
'pompy-ciepla': 'Pompy Ciepła',
'remonty': 'Remonty',
'hydraulika': 'Hydraulika',
'porady': 'Porady'
};
return (
<div className="group relative rounded-2xl overflow-hidden bg-white border border-border shadow-sm hover:shadow-lg transition-all duration-300">
<div className="flex flex-col md:flex-row">
{/* Obraz */}
<div className="relative w-full md:w-3/5 aspect-video md:aspect-auto md:min-h-[400px]">
<Image
src={post.image || 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=1200&q=80'}
alt={post.title}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute top-4 left-4">
<span className="px-3 py-1 bg-accent text-white text-xs font-bold rounded-full uppercase tracking-wider shadow-md">
{categoryLabels[post.category] || post.category}
</span>
</div>
</div>
{/* Treść */}
<div className="p-8 md:p-12 flex flex-col justify-center w-full md:w-2/5">
<div className="flex items-center gap-4 text-text-muted text-sm font-medium mb-4">
<div className="flex items-center gap-1.5">
<Calendar className="w-4 h-4 text-primary/60" />
<span>{formattedDate}</span>
</div>
{post.author && (
<div className="flex items-center gap-1.5">
<User className="w-4 h-4 text-primary/60" />
<span>{post.author}</span>
</div>
)}
</div>
<h3 className="text-2xl md:text-3xl font-extrabold text-primary font-heading leading-tight mb-4 group-hover:text-accent transition-colors">
<Link href={`/blog/${post.slug}`} className="before:absolute before:inset-0">
{post.title}
</Link>
</h3>
<p className="text-text-muted leading-relaxed mb-8 line-clamp-3">
{post.excerpt}
</p>
<div className="flex items-center gap-2 text-primary font-semibold group-hover:text-accent transition-colors mt-auto">
<span>Czytaj pełny artykuł</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</div>
</div>
</div>
</div>
);
}
+39
View File
@@ -0,0 +1,39 @@
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { SectionHeading } from "../common/SectionHeading";
import { ProductCard } from "../shop/ProductCard";
import { getFeaturedProducts } from "@/content/products";
export function FeaturedProducts() {
const featured = getFeaturedProducts(4);
return (
<section className="py-16 md:py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Nasz Asortyment"
title="Polecane klimatyzatory z montażem"
subtitle="Poznaj wybrane modele urządzeń chłodzących i grzewczych od wiodących marek. Zapewniamy kompleksowy montaż."
align="center"
/>
{/* Products Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
{featured.map((product) => (
<ProductCard key={product.slug} product={product} />
))}
</div>
{/* Catalog Link */}
<div className="flex justify-center mt-12">
<Link href="/sklep">
<button className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 group">
<span>Zobacz cały sklep</span>
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
</button>
</Link>
</div>
</div>
</section>
);
}
+93
View File
@@ -0,0 +1,93 @@
import React from 'react';
import Link from 'next/link';
import { cn } from '@/lib/utils';
import { PiggyBank, FileText, CheckCircle2 } from 'lucide-react';
export const FundingSection = () => {
return (
<section className="py-16 bg-accent-light text-primary">
<div className="container mx-auto px-4 md:px-8 max-w-6xl relative">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<div className="inline-flex items-center gap-2 px-3 py-1 bg-white/50 backdrop-blur-sm rounded-full text-sm font-semibold mb-6">
<PiggyBank className="w-4 h-4 text-accent" />
Dotacje i dofinansowania
</div>
<h2 className="text-3xl md:text-4xl font-bold mb-6">
Odbierz nawet do 35 200 zł dotacji na pompę ciepła
</h2>
<p className="text-primary/80 text-lg mb-8">
[PLACEHOLDER - DO AKTUALIZACJI] Obecnie trwają programy rządowe &quot;Czyste Powietrze&quot; oraz &quot;Mój Prąd&quot;, które pozwalają na zwrot znacznej części kosztów inwestycji. Pomożemy Ci przejść przez cały proces formalny i przygotować niezbędne wnioski.
</p>
<ul className="space-y-4 mb-10">
<li className="flex items-start gap-3">
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
<span className="font-medium">Bezpłatny audyt możliwości dofinansowania</span>
</li>
<li className="flex items-start gap-3">
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
<span className="font-medium">Pomoc w wypełnieniu dokumentacji</span>
</li>
<li className="flex items-start gap-3">
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
<span className="font-medium">Możliwość prefinansowania inwestycji</span>
</li>
</ul>
<Link
href="/dofinansowania"
className={cn("btn btn-primary group")}
>
Dowiedz się więcej o dotacjach
</Link>
<p className="text-xs text-primary/60 mt-4 max-w-sm">
* Wysokość dotacji uzależniona jest od progu dochodowego i zakresu termomodernizacji. Skontaktuj się z nami w celu dokładnej wyceny.
</p>
</div>
<div className="relative">
{/* Dekoracyjna karta */}
<div className="bg-white rounded-3xl p-8 shadow-xl relative z-10 border border-white/50">
<div className="flex justify-between items-start mb-8">
<div>
<p className="text-sm text-gray-500 font-semibold mb-1">Twój kosztorys z dotacją</p>
<h4 className="text-xl font-bold text-primary">Przykładowa kalkulacja</h4>
</div>
<div className="w-12 h-12 bg-accent/10 rounded-full flex items-center justify-center">
<FileText className="w-6 h-6 text-accent" />
</div>
</div>
<div className="space-y-4 text-sm">
<div className="flex justify-between pb-4 border-b border-gray-100">
<span className="text-gray-600">Wartość inwestycji</span>
<span className="font-semibold text-gray-900">45 000 zł</span>
</div>
<div className="flex justify-between pb-4 border-b border-gray-100 text-green-600 font-medium">
<span>Dotacja "Czyste Powietrze"</span>
<span>- 20 500 zł</span>
</div>
<div className="flex justify-between pb-4 border-b border-gray-100 text-green-600 font-medium">
<span>Ulga termomodernizacyjna</span>
<span>- 4 600 zł</span>
</div>
<div className="flex justify-between pt-2">
<span className="text-gray-900 font-bold">Ostateczny koszt inwestora</span>
<span className="font-bold text-2xl text-primary">19 900 zł</span>
</div>
</div>
</div>
{/* Background elements */}
<div className="absolute -top-6 -right-6 w-full h-full bg-accent/20 rounded-3xl -z-10 blur-sm"></div>
<div className="absolute -bottom-8 -left-8 w-32 h-32 bg-white/40 rounded-full -z-10 blur-xl"></div>
</div>
</div>
</div>
</section>
);
};
+63
View File
@@ -0,0 +1,63 @@
import React from 'react';
import { Wind, Droplets, Mountain } from 'lucide-react';
const pumpTypes = [
{
title: 'Powietrze - Woda',
description: 'Najpopularniejsze rozwiązanie. Urządzenie pobiera energię z powietrza zewnętrznego i przekazuje ją do wodnej instalacji grzewczej (kaloryfery, podłogówka) oraz podgrzewa wodę użytkową.',
forWho: 'Idealne do nowych domów i większości termomodernizacji (wymiana pieca).',
icon: Droplets,
},
{
title: 'Powietrze - Powietrze',
description: 'Działają na zasadzie klimatyzatorów. Ogrzewają bezpośrednio powietrze w pomieszczeniu, bez udziału instalacji wodnej. Szybko nagrzewają i posiadają funkcję chłodzenia.',
forWho: 'Świetne jako dodatkowe źródło ciepła, do domków letniskowych i mniejszych przestrzeni.',
icon: Wind,
},
{
title: 'Gruntowe',
description: 'Pobierają ciepło z wnętrza ziemi za pomocą kolektorów. Zapewniają najwyższą i najbardziej stabilną wydajność niezależnie od mrozów na zewnątrz.',
forWho: 'Dla inwestorów z dużą działką, oczekujących najwyższej niezawodności i gotowych na wyższe koszty początkowe.',
icon: Mountain,
},
];
export const HeatPumpTypes = () => {
return (
<section className="py-16 bg-gray-50">
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
<div className="text-center max-w-2xl mx-auto mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Rodzaje pomp ciepła</h2>
<p className="text-gray-600">
Wybór odpowiedniej technologii zależy od uwarunkowań Twojego budynku, wielkości działki oraz budżetu.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{pumpTypes.map((type, idx) => {
const Icon = type.icon;
return (
<div key={idx} className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 flex flex-col h-full hover:shadow-md transition-shadow">
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-12 bg-primary/5 rounded-xl flex items-center justify-center text-primary">
<Icon className="w-6 h-6" />
</div>
<h3 className="text-xl font-bold text-primary">{type.title}</h3>
</div>
<p className="text-gray-600 mb-6 text-sm flex-grow">
{type.description}
</p>
<div className="bg-gray-50 rounded-lg p-4 mt-auto">
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Dla kogo?</h4>
<p className="text-sm text-gray-700 font-medium">{type.forWho}</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
+117
View File
@@ -0,0 +1,117 @@
import Link from "next/link";
import Image from "next/image";
import { ArrowRight, Star } from "lucide-react";
export interface HeroSectionProps {
eyebrow?: string;
title?: React.ReactNode;
subtitle?: string;
ctaText?: string;
ctaLink?: string;
secondaryCtaText?: string;
secondaryCtaLink?: string;
backgroundImage?: string;
features?: string[];
}
export function HeroSection({
eyebrow = "HVAC & Usługi Wykończeniowe",
title = (
<>
Klimatyzacja, pompy ciepła<br className="hidden sm:inline" />
<span className="text-accent"> i usługi domowe</span>
</>
),
subtitle = "Oferujemy profesjonalny dobór, czysty montaż i sprawny serwis. Wykończymy Twoje wnętrza i naprawimy instalacje. Solidnie i z polecenia.",
ctaText = "Darmowa wycena",
ctaLink = "/kontakt",
secondaryCtaText = "Zobacz ofertę",
secondaryCtaLink = "/uslugi-domowe",
backgroundImage = "/hero.jpg",
features
}: HeroSectionProps = {}) {
return (
<section className="relative bg-primary min-h-[480px] md:min-h-[600px] flex items-center overflow-hidden">
{/* TODO ZDJĘCIE HERO: realne zdjęcie firmy/montażu, format poziomy min. 1920×1080, plik public/hero.jpg */}
<Image
src={backgroundImage}
alt="Montaż klimatyzacji i pomp ciepła"
fill
priority
sizes="100vw"
className="object-cover"
/>
{/* NAKŁADKA POD TEKST - jednolita, przyciemniająca */}
<div className="absolute inset-0 bg-black/50" />
<div className="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 md:py-24">
<div className="flex flex-col items-start text-left max-w-3xl">
{/* Badge */}
{eyebrow && (
<div className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full bg-accent/20 border border-accent/30 text-white text-xs font-semibold uppercase tracking-wider mb-6 font-heading">
<span className="size-2 rounded-full bg-accent" />
{eyebrow}
</div>
)}
{/* Heading */}
<h1 className="text-3xl md:text-5xl lg:text-6xl font-extrabold tracking-tight font-heading leading-[1.1] text-white">
{title}
</h1>
{/* Subtitle */}
<p className="mt-6 text-base md:text-lg lg:text-xl text-white/90 max-w-2xl leading-relaxed">
{subtitle}
</p>
{/* Optional Features List */}
{features && features.length > 0 && (
<ul className="mt-6 grid grid-cols-2 gap-3 text-white/90 font-medium">
{features.map((feature, idx) => (
<li key={idx} className="flex items-center gap-2">
<Star className="size-4 text-accent fill-accent" />
{feature}
</li>
))}
</ul>
)}
{/* CTA Buttons */}
<div className="mt-10 flex flex-col sm:flex-row items-center gap-4 w-full sm:w-auto">
{ctaText && ctaLink && (
<Link href={ctaLink} className="w-full sm:w-auto group">
<button className="btn btn-primary text-white font-bold w-full sm:w-auto">
<span>{ctaText}</span>
<ArrowRight className="size-5 group-hover:translate-x-1 transition-transform" />
</button>
</Link>
)}
{secondaryCtaText && secondaryCtaLink && (
<Link href={secondaryCtaLink} className="w-full sm:w-auto">
<button className="btn btn-outline btn-primary bg-white font-bold w-full sm:w-auto">
{secondaryCtaText}
</button>
</Link>
)}
</div>
{/* Trust Bar (Hero specific) */}
<div className="mt-8 flex items-center gap-3 text-sm font-medium text-white/80">
<div className="flex items-center text-cta">
<Star className="size-4 fill-current" />
<Star className="size-4 fill-current" />
<Star className="size-4 fill-current" />
<Star className="size-4 fill-current" />
<Star className="size-4 fill-current" />
</div>
<span>
{/* MOCK: do podmiany */}
4,9/5 w Google · 500+ realizacji · 12 lat na rynku
</span>
</div>
</div>
</div>
</section>
);
}
+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import { Clock, Banknote, MapPin } from 'lucide-react';
interface InfoHighlightsProps {
description: string;
highlights: {
icon: 'clock' | 'price' | 'map';
title: string;
description: string;
}[];
}
const iconMap = {
clock: Clock,
price: Banknote,
map: MapPin,
};
export const InfoHighlights = ({ description, highlights }: InfoHighlightsProps) => {
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
<div className="lg:col-span-5">
<h2 className="text-3xl font-bold text-primary mb-6">Szybki montaż, jasne zasady</h2>
<p className="text-gray-600 text-lg leading-relaxed">
{description}
</p>
</div>
<div className="lg:col-span-7 grid grid-cols-1 sm:grid-cols-3 gap-6">
{highlights.map((item, idx) => {
const Icon = iconMap[item.icon];
return (
<div key={idx} className="bg-gray-50 p-6 rounded-xl border border-gray-100 flex flex-col items-center text-center">
<div className="w-12 h-12 bg-accent/10 rounded-full flex items-center justify-center mb-4 text-accent">
<Icon className="w-6 h-6" />
</div>
<h3 className="font-semibold text-primary mb-2">{item.title}</h3>
<p className="text-sm text-gray-500">{item.description}</p>
</div>
);
})}
</div>
</div>
</div>
</section>
);
};
+33
View File
@@ -0,0 +1,33 @@
import React from 'react';
export function MapEmbed() {
return (
<section className="w-full h-[400px] md:h-[500px] bg-slate-200 relative overflow-hidden">
{/*
Docelowo tutaj powinien znaleźć się kod iframe z Google Maps.
Przykład:
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2505.465597792618!2d17.031980815754593!3d51.10006297957077!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x0!2zNTHCsDA2JzAwLjIiTiAxN8KwMDInMDMuMCJF!5e0!3m2!1spl!2spl!4v1620000000000!5m2!1spl!2spl"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen={false}
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
className="absolute inset-0"
></iframe>
*/}
{/* Poniżej znajduje się Placeholder */}
<div className="absolute inset-0 flex items-center justify-center bg-slate-200 border-y border-border">
<div className="text-center p-6 bg-white/80 backdrop-blur-sm rounded-xl border border-white/50 shadow-sm max-w-sm mx-4">
<svg className="w-12 h-12 text-primary/40 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
<p className="font-semibold text-primary font-heading">Mapa Lokalizacji</p>
<p className="text-sm text-text-muted mt-1">Tutaj zostanie osadzona interaktywna mapa Google (iframe).</p>
</div>
</div>
</section>
);
}
+66
View File
@@ -0,0 +1,66 @@
import Link from "next/link";
import { ThermometerSun, Home, ChevronRight, Wrench, ShieldCheck, ArrowRight } from "lucide-react";
export function PillarsSection() {
return (
<section className="py-16 md:py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Pillar 1: Chłód i Ogrzewanie */}
<div className="group relative rounded-2xl border border-border p-8 md:p-12 bg-slate-50 hover:bg-white hover:border-accent hover:shadow-xl transition-all duration-300 flex flex-col justify-between overflow-hidden">
{/* Visual background gradient glow on hover */}
<div className="absolute right-0 top-0 size-48 rounded-full bg-accent/5 filter blur-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div>
<div className="inline-flex p-4 rounded-xl bg-accent-light text-accent mb-6 group-hover:scale-110 transition-transform duration-300">
<ThermometerSun className="size-8" />
</div>
<h3 className="text-2xl md:text-3xl font-bold text-primary font-heading mb-4">
Chłód i Ogrzewanie
</h3>
<p className="text-text-muted text-base leading-relaxed mb-8 max-w-md">
Klimatyzacja i pompy ciepła dla Twojego domu lub biura. Oferujemy profesjonalny audyt, dobór optymalnej mocy, autoryzowany montaż i stałą opiekę serwisową.
</p>
</div>
<div>
<Link href="/klimatyzacja">
<button className="btn btn-primary text-white flex items-center gap-2">
<span>Przejdź do oferty HVAC</span>
<ChevronRight className="size-5 group-hover:translate-x-1 transition-transform" />
</button>
</Link>
</div>
</div>
{/* Pillar 2: Twój Dom */}
<div className="group relative rounded-2xl border border-border p-8 md:p-12 bg-slate-50 hover:bg-white hover:border-cta hover:shadow-xl transition-all duration-300 flex flex-col justify-between overflow-hidden">
{/* Visual background gradient glow on hover */}
<div className="absolute right-0 top-0 size-48 rounded-full bg-cta/5 filter blur-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div>
<div className="inline-flex p-4 rounded-xl bg-cta/10 text-cta mb-6 group-hover:scale-110 transition-transform duration-300">
<Home className="size-8" />
</div>
<h3 className="text-2xl md:text-3xl font-bold text-primary font-heading mb-4">
Twój Dom
</h3>
<p className="text-text-muted text-base leading-relaxed mb-8 max-w-md">
Usługi remontowo-budowlane, prace wykończeniowe łazienek i kuchni, instalacje hydrauliczne oraz kompleksowy serwis sprzętu chłodniczego i AGD.
</p>
</div>
<div className="mt-12 text-center">
<Link href="/uslugi-domowe">
<button className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 mx-auto group">
<span>Pełna oferta usług</span>
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
</button>
</Link>
</div>
</div>
</div>
</div>
</section>
);
}
+150
View File
@@ -0,0 +1,150 @@
"use client";
import React, { useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Post } from '@/types/types';
import { cn } from '@/lib/utils';
import { Calendar, ArrowRight } from 'lucide-react';
interface PostsGridProps {
posts: Post[];
}
const POSTS_PER_PAGE = 6;
const categories = [
{ id: 'wszystkie', label: 'Wszystkie' },
{ id: 'klimatyzacja', label: 'Klimatyzacja' },
{ id: 'pompy-ciepla', label: 'Pompy ciepła' },
{ id: 'remonty', label: 'Remonty' },
{ id: 'hydraulika', label: 'Hydraulika' },
{ id: 'porady', label: 'Porady' },
];
export function PostsGrid({ posts }: PostsGridProps) {
const [activeCategory, setActiveCategory] = useState('wszystkie');
const [visibleCount, setVisibleCount] = useState(POSTS_PER_PAGE);
const filteredPosts = posts.filter(
(post) => activeCategory === 'wszystkie' || post.category === activeCategory
);
const visiblePosts = filteredPosts.slice(0, visibleCount);
const hasMore = visibleCount < filteredPosts.length;
const handleShowMore = () => {
setVisibleCount(prev => prev + POSTS_PER_PAGE);
};
const handleCategoryChange = (categoryId: string) => {
setActiveCategory(categoryId);
setVisibleCount(POSTS_PER_PAGE); // Reset paginacji przy zmianie kategorii
};
const categoryLabels: Record<string, string> = {
'klimatyzacja': 'Klimatyzacja',
'pompy-ciepla': 'Pompy Ciepła',
'remonty': 'Remonty',
'hydraulika': 'Hydraulika',
'porady': 'Porady'
};
return (
<div className="py-12">
{/* Filtry */}
<div className="flex flex-wrap items-center gap-3 mb-12">
{categories.map((category) => (
<button
key={category.id}
onClick={() => handleCategoryChange(category.id)}
className={cn(
"px-4 py-2 rounded-full text-sm font-semibold transition-all duration-300 border",
activeCategory === category.id
? "bg-primary text-white border-primary shadow-md"
: "bg-white text-text-muted border-border hover:border-primary/30 hover:text-primary"
)}
>
{category.label}
</button>
))}
</div>
{/* Siatka Artykułów */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{visiblePosts.map((post) => {
const dateObj = new Date(post.date);
const formattedDate = dateObj.toLocaleDateString('pl-PL', {
day: 'numeric', month: 'long', year: 'numeric'
});
return (
<article
key={post.slug}
className="group flex flex-col bg-white rounded-2xl overflow-hidden border border-border hover:shadow-lg transition-all duration-300"
>
<Link href={`/blog/${post.slug}`} className="relative aspect-[16/10] overflow-hidden">
<Image
src={post.image || 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&q=80'}
alt={post.title}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute top-4 left-4">
<span className="px-3 py-1 bg-accent text-white text-[10px] font-bold rounded-full uppercase tracking-wider shadow-sm">
{categoryLabels[post.category] || post.category}
</span>
</div>
</Link>
<div className="flex flex-col flex-grow p-6 md:p-8">
<div className="flex items-center gap-2 text-xs font-medium text-text-muted mb-3">
<Calendar className="w-3.5 h-3.5 text-primary/50" />
<span>{formattedDate}</span>
</div>
<h3 className="text-xl font-bold text-primary font-heading leading-snug mb-3 group-hover:text-accent transition-colors">
<Link href={`/blog/${post.slug}`}>
{post.title}
</Link>
</h3>
<p className="text-sm text-text-muted leading-relaxed line-clamp-3 mb-6 flex-grow">
{post.excerpt}
</p>
<div className="mt-auto">
<Link
href={`/blog/${post.slug}`}
className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary group-hover:text-accent transition-colors"
>
<span>Czytaj więcej</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
</article>
);
})}
</div>
{visiblePosts.length === 0 && (
<div className="text-center py-16 bg-slate-50 rounded-2xl border border-border">
<p className="text-lg text-text-muted font-medium">Brak artykułów w tej kategorii.</p>
</div>
)}
{/* Pokaż więcej */}
{hasMore && (
<div className="mt-12 flex justify-center">
<button
onClick={handleShowMore}
className="btn btn-outline btn-primary bg-white font-bold"
>
Wczytaj więcej artykułów
</button>
</div>
)}
</div>
);
}
+102
View File
@@ -0,0 +1,102 @@
import React from 'react';
import { pricingData, PricingItem } from '@/content/pricing';
import { Check, Info, ArrowRight, Download } from 'lucide-react';
export const PricingTable = () => {
const splitItems = pricingData.filter((item) => item.category === 'split');
const multisplitItems = pricingData.filter((item) => item.category === 'multisplit');
const additionalItems = pricingData.filter((item) => item.category === 'additional');
const renderTableRows = (items: PricingItem[]) => {
return items.map((item) => (
<tr key={item.id} className="border-b border-gray-100 last:border-0 hover:bg-gray-50/50 transition-colors">
<td className="py-4 px-4 text-gray-800 font-medium">{item.name}</td>
<td className="py-4 px-4 text-center whitespace-nowrap text-gray-600">{item.priceNet} zł</td>
<td className="py-4 px-4 text-center whitespace-nowrap font-semibold text-primary">{item.priceGrossPrivate} zł</td>
<td className="py-4 px-4 text-center whitespace-nowrap text-gray-600 hidden sm:table-cell">{item.priceGrossCompany} zł</td>
</tr>
));
};
return (
<section className="py-16 bg-white" id="cennik">
<div className="container mx-auto px-4 md:px-8 max-w-5xl">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Cennik usług instalacyjnych</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
Przejrzyste ceny bez ukrytych kosztów. Ostateczna wycena zależy od specyfiki budynku i trudności montażu – dokonywana jest zawsze przed rozpoczęciem prac.
</p>
</div>
<div className="overflow-x-auto rounded-xl border border-gray-200 shadow-sm mb-8">
<table className="w-full text-left border-collapse min-w-[600px]">
<thead>
<tr className="bg-gray-50 border-b border-gray-200 text-sm uppercase text-gray-500">
<th className="py-4 px-4 font-semibold w-1/2">Usługa</th>
<th className="py-4 px-4 font-semibold text-center">Cena netto</th>
<th className="py-4 px-4 font-semibold text-center">Brutto (8% VAT)*</th>
<th className="py-4 px-4 font-semibold text-center hidden sm:table-cell">Brutto (23% VAT)</th>
</tr>
</thead>
<tbody>
{/* Split */}
<tr>
<td colSpan={4} className="bg-gray-50/80 py-3 px-4 font-semibold text-primary text-sm uppercase">
Montaż układów SPLIT
</td>
</tr>
{renderTableRows(splitItems)}
{/* Multisplit */}
<tr>
<td colSpan={4} className="bg-gray-50/80 py-3 px-4 font-semibold text-primary text-sm uppercase">
Montaż układów MULTISPLIT
</td>
</tr>
{renderTableRows(multisplitItems)}
{/* Usługi dodatkowe */}
<tr>
<td colSpan={4} className="bg-gray-50/80 py-3 px-4 font-semibold text-primary text-sm uppercase">
Usługi dodatkowe i serwis
</td>
</tr>
{renderTableRows(additionalItems)}
</tbody>
</table>
</div>
<div className="flex flex-col md:flex-row gap-8 items-start justify-between bg-gray-50 p-6 rounded-xl border border-gray-100">
<div className="flex-1">
<h4 className="font-semibold text-primary mb-3 flex items-center gap-2">
<Check className="w-5 h-5 text-accent" />
Co obejmuje montaż podstawowy?
</h4>
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-gray-600">
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Wykonanie przewiertu przez ścianę</li>
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Instalacja chłodnicza do 3 mb</li>
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Podłączenie do gniazdka (kabel 3m)</li>
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Grawitacyjne odprowadzenie skroplin</li>
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Montaż jednostek do wysokości 3m</li>
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Próba szczelności i uruchomienie</li>
</ul>
</div>
<div className="md:w-64 shrink-0 flex flex-col gap-4">
<button className="btn btn-outline btn-primary bg-white w-full justify-between group">
Pobierz cennik PDF
<Download className="w-4 h-4 ml-2 group-hover:-translate-y-1 transition-transform" />
</button>
<div className="text-xs text-gray-500 flex items-start gap-2 bg-white p-3 rounded-lg border border-gray-100">
<Info className="w-4 h-4 shrink-0 text-accent mt-0.5" />
<p>
*Stawka 8% VAT obowiązuje przy montażu w obiektach mieszkalnych (domy do 300m², mieszkania do 150m²).
</p>
</div>
</div>
</div>
</div>
</section>
);
};
+83
View File
@@ -0,0 +1,83 @@
import {
PhoneCall, ClipboardCheck, Cpu, HeartHandshake, HelpCircle,
ShieldAlert, Construction, Layers, MonitorDown, Server, Gauge, Zap, BookOpen
} from "lucide-react";
import { SectionHeading } from "../common/SectionHeading";
import { mainProcessSteps } from "@/content/process";
import { ProcessStep } from "@/types/types";
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
PhoneCall,
ClipboardCheck,
Cpu,
HeartHandshake,
ShieldAlert,
Construction,
Layers,
MonitorDown,
Server,
Gauge,
Zap,
BookOpen,
};
interface ProcessStepsProps {
steps?: ProcessStep[];
title?: string;
subtitle?: string;
eyebrow?: string;
}
export function ProcessSteps({
steps = mainProcessSteps,
title = "Prosty proces współpracy w 4 krokach",
subtitle = "Od momentu kontaktu do pełnego uruchomienia i serwisu — dbamy o każdy detal na każdym etapie.",
eyebrow = "Jak Działamy"
}: ProcessStepsProps) {
return (
<section className="py-16 md:py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow={eyebrow}
title={title}
subtitle={subtitle}
align="center"
/>
{/* Process Steps Timeline */}
<div className="relative mt-12">
{/* Desktop connecting line (only if few steps, otherwise wrap) */}
{steps.length <= 4 && (
<div className="hidden lg:block absolute top-10 left-[12%] right-[12%] h-0.5 bg-slate-200 z-0" />
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-12 gap-x-8 relative z-10">
{steps.map((step, idx) => {
const Icon = iconMap[step.icon || ""] || HelpCircle;
return (
<div key={idx} className="flex flex-col items-center text-center group">
{/* Circle Badge with Icon & Number */}
<div className="relative size-20 rounded-full bg-slate-50 border-2 border-slate-200 flex items-center justify-center text-primary group-hover:border-accent group-hover:bg-accent-light group-hover:text-accent transition-all duration-300 shadow-sm z-10">
{/* Step Number */}
<span className="absolute -top-1.5 -right-1.5 size-6 rounded-full bg-cta text-white text-xs font-bold flex items-center justify-center font-heading border-2 border-white shadow-sm">
{step.number}
</span>
<Icon className="size-8 group-hover:scale-110 transition-transform duration-300" />
</div>
{/* Title & Description */}
<h3 className="text-lg font-bold text-primary font-heading mt-6 mb-2">
{step.title}
</h3>
<p className="text-sm text-text-muted leading-relaxed max-w-[240px]">
{step.description}
</p>
</div>
);
})}
</div>
</div>
</div>
</section>
);
}
+54
View File
@@ -0,0 +1,54 @@
import React from 'react';
import Image from 'next/image';
import { projectsData, Project } from '@/content/projects';
interface ProjectsGalleryProps {
category: Project['category'];
title?: string;
description?: string;
}
export const ProjectsGallery = ({
category,
title = "Nasze realizacje",
description = "Zobacz przykłady prac wykonanych przez naszych certyfikowanych instalatorów."
}: ProjectsGalleryProps) => {
const projects = projectsData.filter((project) => project.category === category);
if (projects.length === 0) return null;
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
<div className="text-center max-w-2xl mx-auto mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">{title}</h2>
<p className="text-gray-600">{description}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{projects.map((project) => (
<div key={project.id} className="group rounded-2xl overflow-hidden border border-gray-100 shadow-sm hover:shadow-lg transition-all duration-300 bg-white flex flex-col h-full">
<div className="relative h-64 overflow-hidden bg-gray-100">
<Image
src={project.imageUrl}
alt={project.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-500"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
<div className="p-6 flex-grow flex flex-col">
<h3 className="font-bold text-lg text-primary mb-2 group-hover:text-accent transition-colors">
{project.title}
</h3>
<p className="text-gray-600 text-sm mb-4 flex-grow">
{project.description}
</p>
</div>
</div>
))}
</div>
</div>
</section>
);
};
+97
View File
@@ -0,0 +1,97 @@
"use client";
import React, { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { projectsData, Project } from '@/content/projects';
import { cn } from '@/lib/utils';
import { MapPin, ArrowRight } from 'lucide-react';
type Category = 'wszystkie' | Project['category'];
const categories: { id: Category; label: string }[] = [
{ id: 'wszystkie', label: 'Wszystkie' },
{ id: 'klimatyzacja', label: 'Klimatyzacja' },
{ id: 'pompy-ciepla', label: 'Pompy ciepła' },
{ id: 'remonty', label: 'Remonty' },
{ id: 'hydraulika', label: 'Hydraulika' },
];
export function ProjectsGrid() {
const [activeCategory, setActiveCategory] = useState<Category>('wszystkie');
const filteredProjects = projectsData.filter(
(project) => activeCategory === 'wszystkie' || project.category === activeCategory
);
return (
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Filtry */}
<div className="flex flex-wrap items-center justify-center gap-3 mb-12">
{categories.map((category) => (
<button
key={category.id}
onClick={() => setActiveCategory(category.id)}
className={cn(
"px-5 py-2.5 rounded-full text-sm font-semibold transition-all duration-300 border",
activeCategory === category.id
? "bg-primary text-white border-primary shadow-md"
: "bg-white text-text-muted border-border hover:border-primary/30 hover:text-primary"
)}
>
{category.label}
</button>
))}
</div>
{/* Siatka */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredProjects.map((project) => (
<div
key={project.id}
className="group relative rounded-2xl overflow-hidden bg-slate-100 aspect-[4/3]"
>
<Image
src={project.imageUrl}
alt={project.title}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
{/* Nakładka gradientowa */}
<div className="absolute inset-0 bg-gradient-to-t from-primary/90 via-primary/40 to-transparent opacity-80 group-hover:opacity-90 transition-opacity duration-300" />
{/* Treść (Zawsze widoczna lokalizacja i kategoria, pojawiający się opis) */}
<div className="absolute inset-0 p-6 flex flex-col justify-end">
<div className="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
<div className="flex items-center gap-3 mb-3">
<span className="px-3 py-1 bg-accent text-white text-xs font-bold rounded-full uppercase tracking-wider">
{categories.find(c => c.id === project.category)?.label}
</span>
<div className="flex items-center gap-1 text-white/80 text-sm font-medium">
<MapPin className="w-3.5 h-3.5" />
<span>{project.location}</span>
</div>
</div>
<h3 className="text-xl font-bold text-white mb-2 font-heading">
{project.title}
</h3>
<p className="text-white/80 text-sm line-clamp-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 delay-100">
{project.description}
</p>
</div>
</div>
</div>
))}
</div>
{filteredProjects.length === 0 && (
<div className="text-center py-20 text-text-muted">
<p className="text-lg">Brak realizacji w tej kategorii.</p>
</div>
)}
</section>
);
}
+47
View File
@@ -0,0 +1,47 @@
import { SectionHeading } from "../common/SectionHeading";
import { IconCard } from "../common/IconCard";
import { services as defaultServices } from "@/content/services";
interface ServicesGridProps {
services?: {
title: string;
description: string;
icon: string;
href?: string;
}[];
title?: string;
subtitle?: string;
eyebrow?: string;
}
export function ServicesGrid({
services = defaultServices,
title = "Kompleksowe usługi dla Twojego domu",
subtitle = "Zapewniamy profesjonalne wsparcie w zakresie instalacji sanitarnych, grzewczych, chłodniczych oraz prac wykończeniowych.",
eyebrow = "Pełna Oferta"
}: ServicesGridProps) {
return (
<section className="py-16 md:py-24 bg-slate-50 border-t border-b border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow={eyebrow}
title={title}
subtitle={subtitle}
align="center"
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mt-10">
{services.map((service, idx) => (
<IconCard
key={idx}
title={service.title}
description={service.description}
iconName={service.icon}
href={service.href}
/>
))}
</div>
</div>
</section>
);
}
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import { companyDetails } from '@/content/company';
export function StatsBar() {
return (
<section className="bg-primary text-white py-12 md:py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-4 divide-x divide-white/10">
{companyDetails.stats.map((stat, idx) => (
<div key={idx} className="flex flex-col items-center justify-center text-center px-4">
<div className="text-4xl md:text-5xl font-extrabold text-accent mb-2 font-heading">
{stat.value}
</div>
<div className="text-sm md:text-base font-medium text-white/80 uppercase tracking-wider">
{stat.label}
</div>
</div>
))}
</div>
</div>
</section>
);
}
+64
View File
@@ -0,0 +1,64 @@
import React from 'react';
import { ThermometerSnowflake, VolumeX, Droplets, Snowflake, PowerOff } from 'lucide-react';
const symptoms = [
{
icon: ThermometerSnowflake,
title: 'Lodówka nie chłodzi',
description: 'Najczęstsza usterka. Może oznaczać problem z termostatem, wyciek czynnika chłodniczego lub uszkodzenie sprężarki.',
},
{
icon: VolumeX,
title: 'Głośna praca / Dziwne dźwięki',
description: 'Buczenie, stukanie lub piszczenie często wskazuje na zużycie wentylatora, agregatu lub problem z ułożeniem urządzenia.',
},
{
icon: Droplets,
title: 'Zbieranie się wody na dnie',
description: 'Zazwyczaj spowodowane zatkanym odpływem skroplin w chłodziarce lub problemem z systemem No Frost.',
},
{
icon: Snowflake,
title: 'Nadmierne oszranianie',
description: 'Brak uszczelnienia drzwi, uszkodzona grzałka odszraniania lub awaria czujnika temperatury.',
},
{
icon: PowerOff,
title: 'Urządzenie nie włącza się',
description: 'Problemy z zasilaniem, uszkodzony moduł sterujący (płyta główna) lub całkowita awaria agregatu.',
},
];
export const SymptomsList = () => {
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-6xl">
<div className="text-center max-w-2xl mx-auto mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Typowe usterki — kiedy wezwać serwis?</h2>
<p className="text-gray-600">
Jeśli zauważysz którykolwiek z poniższych objawów, nie zwlekaj. Szybka reakcja może zapobiec poważniejszej awarii i uchronić żywność przed zepsuciem.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{symptoms.map((symptom, idx) => {
const Icon = symptom.icon;
return (
<div key={idx} className="bg-gray-50 p-6 rounded-xl border border-gray-100 flex gap-4 items-start hover:shadow-md transition-shadow">
<div className="w-12 h-12 shrink-0 bg-accent/10 rounded-lg flex items-center justify-center text-accent mt-1">
<Icon className="w-6 h-6" />
</div>
<div>
<h3 className="font-bold text-primary mb-2">{symptom.title}</h3>
<p className="text-gray-600 text-sm leading-relaxed">
{symptom.description}
</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
+57
View File
@@ -0,0 +1,57 @@
import { Star, Quote } from "lucide-react";
import { SectionHeading } from "../common/SectionHeading";
import { testimonials } from "@/content/testimonials";
export function Testimonials() {
return (
<section className="py-16 md:py-24 bg-slate-50 border-t border-b border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Opinie Klientów"
title="Co mówią o nas nasi klienci?"
subtitle="Zadowolenie klientów to nasz najwyższy cel. Zobacz opinie osób, które zaufały naszym instalatorom i ekipom budowlanym."
align="center"
/>
{/* Testimonials Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{testimonials.map((test, idx) => (
<div
key={idx}
className="relative p-8 rounded-xl bg-white border border-border hover:shadow-md transition-shadow flex flex-col justify-between"
>
{/* Quote icon background decoration */}
<Quote className="absolute top-6 right-6 size-12 text-slate-100 -z-0" />
<div className="relative z-10 flex flex-col gap-4">
{/* Stars */}
<div className="flex gap-1 text-cta">
{Array.from({ length: test.rating }).map((_, iIdx) => (
<Star key={iIdx} className="size-4 fill-cta text-cta" />
))}
</div>
{/* Review Text */}
<p className="text-sm md:text-base text-primary italic leading-relaxed">
&ldquo;{test.text}&rdquo;
</p>
</div>
{/* Reviewer Details */}
<div className="relative z-10 border-t border-slate-100 mt-6 pt-4 flex justify-between items-center text-xs">
<span className="font-bold text-primary font-heading">
{test.name}
</span>
{test.source && (
<span className="px-2.5 py-1 rounded bg-slate-100 text-text-muted font-medium">
{test.source}
</span>
)}
</div>
</div>
))}
</div>
</div>
</section>
);
}
+54
View File
@@ -0,0 +1,54 @@
import React from 'react';
import { Home, Zap, Wrench } from 'lucide-react';
const cases = [
{
icon: Home,
title: 'Nowo budowany dom',
description: 'Najlepszy moment na decyzję o pompie ciepła. Pozwala to na uniknięcie budowy komina, przyłącza gazowego i wydzielania kotłowni. Idealnie współpracuje z nowoczesnym ogrzewaniem podłogowym.',
},
{
icon: Wrench,
title: 'Wymiana starego kotła (kopciucha)',
description: 'Zastąpienie uciążliwego pieca węglowego bezobsługową pompą. Często wymaga dostosowania istniejącej instalacji, ale daje dostęp do najwyższych poziomów dofinansowania z programu Czyste Powietrze.',
},
{
icon: Zap,
title: 'Modernizacja i hybryda',
description: 'Dołożenie pompy ciepła do istniejącego, wciąż sprawnego kotła gazowego lub olejowego. Pompa przejmuje ogrzewanie przez większość sezonu, a stary kocioł wspomaga system tylko w największe mrozy.',
},
];
export const UseCases = () => {
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-5xl">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Kiedy warto zainwestować?</h2>
<p className="text-gray-600">
Pompa ciepła to elastyczne rozwiązanie, które sprawdzi się w różnych scenariuszach życiowych i inwestycyjnych.
</p>
</div>
<div className="space-y-6">
{cases.map((useCase, idx) => {
const Icon = useCase.icon;
return (
<div key={idx} className="flex flex-col sm:flex-row gap-6 p-6 md:p-8 rounded-2xl border border-gray-100 hover:border-accent/30 transition-colors bg-gray-50/50 hover:bg-white group">
<div className="w-16 h-16 shrink-0 bg-white border border-gray-100 shadow-sm rounded-2xl flex items-center justify-center text-primary group-hover:text-accent group-hover:border-accent/20 transition-all">
<Icon className="w-8 h-8" />
</div>
<div>
<h3 className="text-xl font-bold text-primary mb-3">{useCase.title}</h3>
<p className="text-gray-600 leading-relaxed">
{useCase.description}
</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
+56
View File
@@ -0,0 +1,56 @@
import React from 'react';
import { SectionHeading } from '../common/SectionHeading';
import { ShieldCheck, Clock, ThumbsUp, HeartHandshake } from 'lucide-react';
const values = [
{
title: 'Najwyższa jakość',
description: 'Nie idziemy na kompromisy. Stosujemy wyłącznie sprawdzone materiały i urządzenia renomowanych producentów.',
icon: ThumbsUp
},
{
title: 'Terminowość',
description: 'Szanujemy Twój czas. Każde zlecenie realizujemy zgodnie z ustalonym harmonogramem.',
icon: Clock
},
{
title: 'Bezpieczeństwo',
description: 'Posiadamy wszystkie niezbędne uprawnienia (m.in. F-Gaz, SEP) oraz pełne ubezpieczenie OC firmy.',
icon: ShieldCheck
},
{
title: 'Długofalowe relacje',
description: 'Zależy nam na zaufaniu. Po skończonym montażu czy remoncie oferujemy pełną opiekę serwisową.',
icon: HeartHandshake
}
];
export function ValuesGrid() {
return (
<section className="py-16 md:py-24 bg-slate-50 border-y border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Dlaczego my"
title="Nasze wartości"
subtitle="Zasady, którymi kierujemy się w codziennej pracy z naszymi klientami."
align="center"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 mt-16">
{values.map((val, idx) => {
const Icon = val.icon;
return (
<div key={idx} className="bg-white p-8 rounded-2xl border border-border text-center hover:shadow-lg transition-shadow duration-300">
<div className="mx-auto w-16 h-16 bg-accent/10 text-accent rounded-2xl flex items-center justify-center mb-6 transform -rotate-3 hover:rotate-0 transition-transform">
<Icon className="w-8 h-8" />
</div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">{val.title}</h3>
<p className="text-text-muted leading-relaxed text-sm">{val.description}</p>
</div>
);
})}
</div>
</div>
</section>
);
}
+89
View File
@@ -0,0 +1,89 @@
import { Award, FileText, ShieldCheck, Sparkles } from "lucide-react";
import { companyDetails } from "@/content/company";
export function WhyUs() {
const advantages = [
{
icon: Award,
title: "Uprawnienia F-Gaz i certyfikaty",
description: "Posiadamy wszystkie wymagane certyfikaty UDT oraz autoryzacje wiodących producentów klimatyzacji.",
},
{
icon: FileText,
title: "Jasna wycena bez ukrytych kosztów",
description: "Przed montażem otrzymujesz pełny kosztorys. Ceny podane w ofercie są cenami ostatecznymi.",
},
{
icon: ShieldCheck,
title: "Gwarancja i wsparcie serwisowe",
description: "Zapewniamy pełną opiekę pogwarancyjną oraz coroczne przeglądy zachowujące gwarancję producenta.",
},
{
icon: Sparkles,
title: "Czystość i dbałość o detale",
description: "Pracujemy z użyciem odkurzaczy przemysłowych. Po montażu zostawiamy pomieszczenia idealnie czyste.",
},
];
return (
<section className="py-16 md:py-24 bg-slate-50 border-t border-b border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-center">
{/* Left Column: Title, Intro and Statistics */}
<div className="lg:col-span-5 flex flex-col gap-6">
<div>
<span className="text-xs md:text-sm font-semibold uppercase tracking-wider text-cta font-heading">
Dlaczego AeroService
</span>
<h2 className="text-3xl md:text-4xl font-bold text-primary font-heading leading-tight mt-2">
Zaufany partner w instalacjach domowych
</h2>
<p className="text-sm md:text-base text-text-muted mt-4 leading-relaxed">
Stawiamy na najwyższą jakość wykonania, terminowość oraz bezkompromisowe bezpieczeństwo. Pomagamy na każdym kroku — od wyboru odpowiedniego urządzenia, poprzez załatwienie dotacji, aż po finalny montaż i regularny serwis.
</p>
</div>
{/* Stats Counters Grid */}
<div className="grid grid-cols-2 gap-6 mt-6 border-t border-border pt-8">
{companyDetails.stats.map((stat, idx) => (
<div key={idx} className="flex flex-col">
<span className="text-3xl md:text-4xl font-extrabold text-primary font-heading">
{stat.value}
</span>
<span className="text-xs md:text-sm text-text-muted mt-1 leading-snug">
{stat.label}
</span>
</div>
))}
</div>
</div>
{/* Right Column: Advantages Grid */}
<div className="lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-6">
{advantages.map((adv, idx) => {
const Icon = adv.icon;
return (
<div
key={idx}
className="p-6 rounded-xl bg-white border border-border hover:shadow-md transition-shadow flex flex-col gap-4"
>
<div className="size-12 rounded-lg bg-accent-light text-accent flex items-center justify-center shrink-0">
<Icon className="size-6" />
</div>
<div>
<h3 className="text-base font-bold text-primary font-heading">
{adv.title}
</h3>
<p className="text-xs md:text-sm text-text-muted mt-1.5 leading-relaxed">
{adv.description}
</p>
</div>
</div>
);
})}
</div>
</div>
</div>
</section>
);
}
+120
View File
@@ -0,0 +1,120 @@
"use client";
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { useCartStore } from '@/lib/cart-store';
import { formatCurrency } from '@/lib/utils';
import { Minus, Plus, Trash2, Wrench } from 'lucide-react';
export function CartItems() {
const { items, removeItem, updateQuantity, toggleInstall } = useCartStore();
if (items.length === 0) {
return (
<div className="bg-white p-12 rounded-2xl border border-border text-center">
<div className="w-20 h-20 bg-slate-50 text-slate-300 rounded-full flex items-center justify-center mx-auto mb-6">
<Trash2 className="w-10 h-10" />
</div>
<h2 className="text-2xl font-bold text-primary font-heading mb-4">Twój koszyk jest pusty</h2>
<p className="text-text-muted mb-8">Nie dodałeś jeszcze żadnych produktów do koszyka.</p>
<Link href="/sklep" className="inline-flex bg-cta hover:bg-cta-hover text-white font-bold py-3 px-8 rounded-xl transition-colors">
Przejdź do sklepu
</Link>
</div>
);
}
return (
<div className="space-y-6">
{items.map((item) => {
const itemPrice = item.withInstall ? item.priceWithInstall : item.price;
const totalGross = itemPrice * item.quantity;
return (
<div key={item.id} className="bg-white p-6 rounded-2xl border border-border flex flex-col sm:flex-row gap-6 relative">
{/* Usuń pozycję */}
<button
onClick={() => removeItem(item.id)}
className="absolute top-4 right-4 p-2 text-text-muted hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
aria-label="Usuń z koszyka"
>
<Trash2 className="w-5 h-5" />
</button>
{/* Obraz */}
<div className="relative w-24 h-24 sm:w-32 sm:h-32 shrink-0 bg-slate-50 rounded-xl p-2 border border-border">
<Image
src={item.image}
alt={item.name}
fill
className="object-contain"
/>
</div>
{/* Treść */}
<div className="flex flex-col flex-grow justify-between">
<div>
<div className="text-xs font-bold text-text-muted uppercase tracking-wider mb-1">
{item.brand}
</div>
<h3 className="text-lg font-bold text-primary font-heading leading-tight mb-2 pr-10">
<Link href={`/sklep/${item.slug}`} className="hover:text-accent transition-colors">
{item.name}
</Link>
</h3>
{/* Wariant mocowy */}
{(item.label || item.power) && (
<div className="text-sm text-text-muted mb-3">
Wariant: <span className="font-semibold text-primary">{item.label || `${item.power} kW`}</span>
</div>
)}
</div>
{/* Opcja Montażu i Cena jednostkowa */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mt-4">
<label className="flex items-center gap-2 cursor-pointer group">
<div className="relative flex items-center">
<input
type="checkbox"
checked={item.withInstall}
onChange={() => toggleInstall(item.id)}
className="w-5 h-5 rounded border-border text-green-600 focus:ring-green-600"
/>
</div>
<span className={`text-sm font-semibold flex items-center gap-1.5 ${item.withInstall ? 'text-green-700' : 'text-text-muted group-hover:text-primary'}`}>
<Wrench className="w-4 h-4" />
Z montażem
</span>
</label>
{/* Ilość i Cena */}
<div className="flex items-center gap-6">
<div className="flex items-center border border-border rounded-lg bg-slate-50">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
disabled={item.quantity <= 1}
className="w-8 h-8 flex items-center justify-center text-text-muted hover:text-primary disabled:opacity-50"
>
<Minus className="w-4 h-4" />
</button>
<span className="w-8 text-center text-sm font-bold">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="w-8 h-8 flex items-center justify-center text-text-muted hover:text-primary"
>
<Plus className="w-4 h-4" />
</button>
</div>
<div className="text-xl font-extrabold text-primary font-heading w-24 text-right">
{formatCurrency(totalGross)}
</div>
</div>
</div>
</div>
</div>
);
})}
</div>
);
}
+83
View File
@@ -0,0 +1,83 @@
"use client";
import React, { useState } from 'react';
import { useCartStore } from '@/lib/cart-store';
import { formatCurrency } from '@/lib/utils';
import Link from 'next/link';
import { ShieldCheck, ArrowRight, Tag } from 'lucide-react';
export function CartSummary() {
const { items, getTotals } = useCartStore();
const [promoCode, setPromoCode] = useState('');
if (items.length === 0) return null;
const { subtotal, vat, total } = getTotals();
// Obliczamy ile pozycji ma montaż by wyświetlić odpowiednią informację
const itemsWithInstall = items.filter(i => i.withInstall).length;
return (
<div className="bg-slate-50 p-6 md:p-8 rounded-2xl border border-border sticky top-24">
<h3 className="text-xl font-bold text-primary font-heading mb-6">Podsumowanie zamówienia</h3>
{/* Kod rabatowy */}
<div className="mb-8">
<label htmlFor="promo" className="text-sm font-semibold text-text-muted mb-2 block">Posiadasz kod rabatowy?</label>
<div className="flex gap-2">
<div className="relative flex-grow">
<Tag className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
id="promo"
type="text"
value={promoCode}
onChange={(e) => setPromoCode(e.target.value)}
placeholder="Wprowadź kod"
className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-border focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-all text-sm"
/>
</div>
<button className="btn btn-outline btn-primary bg-white shrink-0">Zastosuj</button>
</div>
</div>
{/* Kwoty */}
<div className="space-y-4 mb-6 border-b border-border/50 pb-6">
<div className="flex justify-between text-sm text-text-muted">
<span>Suma netto</span>
<span className="font-semibold text-primary">{formatCurrency(subtotal)}</span>
</div>
<div className="flex justify-between text-sm text-text-muted">
<span>Podatek VAT (mieszany)</span>
<span className="font-semibold text-primary">{formatCurrency(vat)}</span>
</div>
{itemsWithInstall > 0 && (
<div className="text-xs text-green-700 bg-green-50 p-2 rounded border border-green-100 mt-2">
Zastosowano obniżoną stawkę VAT 8% dla {itemsWithInstall} poz. z montażem (dla os. fizycznych).
</div>
)}
</div>
{/* Total */}
<div className="flex justify-between items-end mb-8">
<span className="text-lg font-bold text-primary">Do zapłaty brutto</span>
<span className="text-3xl font-extrabold text-primary font-heading text-right">{formatCurrency(total)}</span>
</div>
{/* Przycisk (Placeholder - brak właściwego checkoutu) */}
<Link href="/zamowienie" className="block w-full">
<button className="btn btn-primary w-full font-bold group cursor-pointer text-white">
Przejdź do zamówienia
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
</button>
</Link>
{/* Zaufanie */}
<div className="mt-6 flex flex-col gap-3">
<div className="flex items-center gap-2 text-xs text-text-muted font-medium justify-center">
<ShieldCheck className="w-4 h-4 text-green-600" />
Bezpieczne zakupy z certyfikatem SSL
</div>
</div>
</div>
);
}
+82
View File
@@ -0,0 +1,82 @@
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Product } from '@/types/types';
import { Heart, Wrench } from 'lucide-react';
import { formatCurrency } from '@/lib/utils';
interface ProductCardProps {
product: Product;
}
export function ProductCard({ product }: ProductCardProps) {
// Jeśli ma warianty, przycisk prowadzi do karty z wariantami
const hasVariants = product.variants && product.variants.length > 0;
return (
<div className="group flex flex-col bg-white rounded-2xl border border-border overflow-hidden hover:shadow-xl transition-all duration-300">
{/* Obraz */}
<Link href={`/sklep/${product.slug}`} className="relative aspect-square p-6 bg-slate-50 flex items-center justify-center overflow-hidden">
{/* Placeholder tagów */}
<div className="absolute top-4 left-4 z-10 flex flex-col gap-2">
{product.features?.slice(0, 2).map((feat, idx) => (
<span key={idx} className="bg-white/90 backdrop-blur-sm text-primary text-[10px] font-bold px-2 py-1 rounded shadow-sm border border-border/50 uppercase">
{feat}
</span>
))}
</div>
{/* Serce */}
<button className="absolute top-4 right-4 z-10 p-2 bg-white/90 backdrop-blur-sm rounded-full text-text-muted hover:text-red-500 hover:bg-white transition-colors shadow-sm">
<Heart className="w-5 h-5" />
</button>
<Image
src={product.image || 'https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80'}
alt={product.name}
width={400}
height={400}
className="object-contain w-full h-full transition-transform duration-500 group-hover:scale-105"
/>
</Link>
{/* Treść */}
<div className="flex flex-col flex-grow p-6">
<div className="text-xs font-bold text-text-muted uppercase tracking-wider mb-1">
{product.brand}
</div>
<h3 className="text-xl font-bold text-primary font-heading leading-tight mb-2 group-hover:text-accent transition-colors">
<Link href={`/sklep/${product.slug}`}>
{product.name}
</Link>
</h3>
{/* Informacja o mocy */}
<div className="text-sm font-medium text-text-muted mb-4">
Moc chłodnicza: <span className="text-primary">{product.power} kW</span>
</div>
{/* Ceny */}
<div className="mt-auto space-y-3">
<div className="flex flex-col gap-1">
<div className="text-2xl font-extrabold text-primary font-heading">
{formatCurrency(product.price)} <span className="text-sm text-text-muted font-normal">samo urządzenie</span>
</div>
{product.priceWithInstall && (
<div className="flex items-center gap-1.5 text-sm font-bold text-green-600 bg-green-50 px-2 py-1 rounded w-fit">
<Wrench className="w-3.5 h-3.5" />
od {formatCurrency(product.priceWithInstall)} z montażem
</div>
)}
</div>
<Link href={`/sklep/${product.slug}`} className="block mt-4">
<button className="btn btn-outline btn-primary bg-white w-full font-semibold">
{hasVariants ? 'Wybierz wariant' : 'Zobacz szczegóły'}
</button>
</Link>
</div>
</div>
</div>
);
}
+55
View File
@@ -0,0 +1,55 @@
"use client";
import React, { useState } from 'react';
import Image from 'next/image';
interface ProductGalleryProps {
image: string;
name: string;
images?: string[]; // opcjonalna dodatkowa galeria z contentu, u nas na razie w mocku mamy 1 image
}
export function ProductGallery({ image, name, images }: ProductGalleryProps) {
const [mainImage, setMainImage] = useState(image || 'https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80');
// Tworzymy tablicę miniatur, używamy placeholdera jako drugiej by pokazać funkcjonalność
const gallery = images && images.length > 0
? images
: [image, 'https://images.unsplash.com/photo-1584622650111-993a426fbf0a?w=800&q=80'];
return (
<div className="flex flex-col gap-4">
{/* Główne zdjęcie */}
<div className="relative aspect-square w-full bg-slate-50 rounded-2xl border border-border overflow-hidden flex items-center justify-center p-8">
<Image
src={mainImage}
alt={name}
width={800}
height={800}
className="object-contain w-full h-full"
priority
/>
</div>
{/* Miniatury */}
{gallery.length > 1 && (
<div className="flex gap-4 overflow-x-auto pb-2">
{gallery.map((img, idx) => (
<button
key={idx}
onClick={() => setMainImage(img)}
className={`relative w-24 h-24 shrink-0 rounded-xl border-2 overflow-hidden bg-slate-50 p-2 transition-all ${mainImage === img ? 'border-primary' : 'border-border hover:border-primary/50'}`}
>
<Image
src={img}
alt={`${name} - miniatura ${idx + 1}`}
fill
className="object-contain"
/>
</button>
))}
</div>
)}
</div>
);
}
+276
View File
@@ -0,0 +1,276 @@
"use client";
import React, { useState } from 'react';
import { Product } from '@/types/types';
import { formatCurrency } from '@/lib/utils';
import { useCartStore } from '@/lib/cart-store';
import { ShoppingCart, Heart, Scale, ShieldCheck, Truck, Clock, AlertCircle, Wrench, ChevronDown, Check, Zap, Volume2, ThermometerSun, Settings, ArrowRightLeft, Sun, Mail, Info, Snowflake, CreditCard } from 'lucide-react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
interface ProductInfoProps {
product: Product;
}
export function ProductInfo({ product }: ProductInfoProps) {
const router = useRouter();
const addItem = useCartStore((state) => state.addItem);
// Wariant
const hasVariants = product.variants && product.variants.length > 0;
const defaultVariant = hasVariants ? product.variants![0] : null;
const [selectedVariant, setSelectedVariant] = useState(defaultVariant);
const [hoursPerDay, setHoursPerDay] = useState<number>(1);
// Aktualnie wybrana cena
const currentPrice = selectedVariant ? selectedVariant.price : product.price;
const currentPriceWithInstall = selectedVariant ? selectedVariant.priceWithInstall : product.priceWithInstall;
const currentPower = selectedVariant ? selectedVariant.power : product.power;
const currentLabel = selectedVariant ? selectedVariant.label : undefined;
const [addedMessage, setAddedMessage] = useState('');
// Kalkulator energii
const costPerKwh = 1.15; // Przykładowy koszt 1 kWh w PLN
const consumptionCooling = currentPower / 4;
const consumptionHeating = currentPower / 3.8;
const costHourCooling = consumptionCooling * costPerKwh;
const costHourHeating = consumptionHeating * costPerKwh;
const monthlyCostCooling = costHourCooling * hoursPerDay * 30;
const monthlyCostHeating = costHourHeating * hoursPerDay * 30;
const handleAddToCart = (withInstall: boolean) => {
addItem({
slug: product.slug,
name: product.name,
brand: product.brand,
image: product.image || '/hero.jpg',
power: currentPower,
label: currentLabel,
price: currentPrice,
priceWithInstall: currentPriceWithInstall || currentPrice,
withInstall
});
setAddedMessage(withInstall ? 'Dodano z montażem' : 'Dodano do koszyka');
setTimeout(() => setAddedMessage(''), 3000);
};
return (
<div className="flex flex-col">
<div className="flex justify-between items-start mb-2">
<span className="text-sm font-bold text-text-muted uppercase tracking-wider">{product.brand}</span>
<div className="flex items-center gap-2 text-text-muted">
<button className="btn btn-ghost btn-circle btn-sm hover:text-accent" aria-label="Dodaj do listy życzeń">
<Heart className="w-5 h-5" />
</button>
<button className="btn btn-ghost btn-circle btn-sm hover:text-accent" aria-label="Porównaj">
<ArrowRightLeft className="w-5 h-5" />
</button>
</div>
</div>
<h1 className="text-3xl md:text-4xl font-extrabold text-primary font-heading leading-tight mb-4">
{product.name}
</h1>
{/* Cechy skrócone */}
{product.features && (
<div className="flex flex-wrap gap-2 mb-8">
{product.features.map((feat, idx) => (
<span key={idx} className="bg-slate-100 text-text-muted text-xs font-semibold px-2.5 py-1 rounded-md border border-border">
{feat}
</span>
))}
</div>
)}
{/* Wybór Wariantu */}
{hasVariants && (
<div className="mb-6">
<span className="block text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">Wybierz wariant mocy:</span>
<div className="flex flex-wrap gap-2">
{product.variants!.map((variant, idx) => (
<button
key={idx}
onClick={() => setSelectedVariant(variant)}
className={`px-4 py-2 rounded-lg border text-sm font-bold transition-all ${
selectedVariant?.label === variant.label
? 'border-primary bg-primary text-white shadow-md'
: 'border-border bg-white text-text-muted hover:border-primary/50 hover:text-primary'
}`}
>
{variant.label}
</button>
))}
</div>
</div>
)}
{/* Szybka specyfikacja z ikonami */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6 text-sm">
<div className="flex items-center gap-2 text-primary font-semibold">
<Snowflake className="w-4 h-4 text-accent" />
<span>{product.specs?.["Klasa energetyczna chłodzenie/grzanie"]?.split("/")[0]?.trim() || "A++"}</span>
</div>
{product.heating !== false && (
<div className="flex items-center gap-2 text-primary font-semibold">
<Sun className="w-4 h-4 text-orange-500" />
<span>{product.specs?.["Klasa energetyczna chłodzenie/grzanie"]?.split("/")[1]?.trim() || "A+"}</span>
</div>
)}
<div className="flex items-center gap-2 text-primary font-semibold">
<Zap className="w-4 h-4 text-yellow-500" />
<span>{(currentPower / 4).toFixed(2)} kW/h</span>
</div>
<div className="flex items-center gap-2 text-primary font-semibold">
<Zap className="w-4 h-4 text-yellow-500" />
<span>{currentPower} kW</span>
</div>
</div>
{/* Szczegółowa specyfikacja skrócona (lista) */}
<ul className="space-y-2.5 mb-8 text-sm text-text-muted">
<li className="flex gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
<span><strong className="text-primary font-semibold">Wydajność chłodnicza:</strong> {(currentPower).toFixed(2)} kW</span>
</li>
{product.heating !== false && (
<li className="flex gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
<span><strong className="text-primary font-semibold">Wydajność grzewcza:</strong> {(currentPower * 1.1).toFixed(2)} kW</span>
</li>
)}
<li className="flex gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
<span><strong className="text-primary font-semibold">Średnie zużycie energii (chłodzenie/grzanie):</strong> {(consumptionCooling).toFixed(2)} / {(consumptionHeating).toFixed(2)} kW</span>
</li>
<li className="flex gap-2 flex-col">
<div className="flex gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
<span><strong className="text-primary font-semibold">Szacunkowy koszt energii – 1 h (chłodzenie/grzanie):</strong> {costHourCooling.toFixed(2)} / {costHourHeating.toFixed(2)} zł</span>
</div>
<div className="ml-3.5 mt-2 p-4 bg-white rounded-xl border border-border shadow-sm inline-block w-full max-w-sm">
<div className="flex items-center justify-between gap-4 mb-3">
<span className="text-sm font-medium text-text-muted">Ile godzin dziennie używasz?</span>
<input
type="number"
min="1"
max="24"
value={hoursPerDay}
onChange={(e) => setHoursPerDay(Number(e.target.value) || 1)}
className="w-16 px-2 py-1.5 text-sm font-bold text-center border border-border rounded-lg outline-none focus:border-primary transition-colors"
/>
</div>
<div className="text-xs text-text-muted leading-relaxed">
Twój miesięczny koszt wyniesie: <br />
<strong className="text-primary font-bold">Chłodzenie {monthlyCostCooling.toFixed(2)} zł</strong> / <strong className="text-primary font-bold">Grzanie {monthlyCostHeating.toFixed(2)} zł</strong>
</div>
</div>
</li>
<li className="flex gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
<span><strong className="text-primary font-semibold">SEER / SCOP:</strong> {product.specs?.["SEER / SCOP"] || "6.10 / 4.00"}</span>
</li>
<li className="flex gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
<span><strong className="text-primary font-semibold">Klasa energetyczna (chłodzenie/grzanie):</strong> {product.specs?.["Klasa energetyczna chłodzenie/grzanie"] || "A++ / A+"}</span>
</li>
<li className="flex gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
<span><strong className="text-primary font-semibold">Poziom głośności jednostki wewnętrznej:</strong> {product.specs?.["Poziom hałasu min/max (dB)"] || "24–38 dB"}</span>
</li>
<li className="flex gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
<span><strong className="text-primary font-semibold">Zakres pracy - chłodzenie:</strong> {product.specs?.["Zakres temperatur zewnętrznych - chłodzenie"] || "-15~43 °C"}</span>
</li>
<li className="flex gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
<span><strong className="text-primary font-semibold">Czas gwarancji:</strong> 5 lat</span>
</li>
{product.features && (
<li className="flex gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
<span><strong className="text-primary font-semibold">Funkcje:</strong> {product.features.join(", ")}</span>
</li>
)}
</ul>
{/* Dostępność i Kontakt */}
<div className="flex items-center gap-6 py-4 border-y border-border mb-8">
<div className="flex items-center gap-2">
<div className="w-3.5 h-3.5 rounded-full bg-green-500 shadow-sm shadow-green-200"></div>
<span className="text-sm font-medium text-primary">Dostępny od ręki</span>
</div>
<Link href="/kontakt" className="flex items-center gap-2 text-text-muted hover:text-primary transition-colors">
<Mail className="w-4 h-4" />
<span className="text-sm">Zapytaj o produkt</span>
</Link>
</div>
{/* Ceny i CTA */}
<div className="bg-slate-50 p-6 rounded-2xl border border-border mb-8 space-y-6">
{/* Cena z montażem (Promowana) */}
{currentPriceWithInstall && (
<div className="flex flex-col gap-3 pb-6 border-b border-border/50">
<div className="flex justify-between items-end flex-wrap gap-4">
<div>
<span className="flex items-center gap-1 text-text-muted text-xs font-semibold mb-1 uppercase tracking-wider">
Cena z montażem dla konsumenta (8% VAT) <Info className="w-3 h-3" />
</span>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-extrabold text-primary font-heading">{formatCurrency(currentPriceWithInstall)}</span>
</div>
<div className="text-xs text-accent font-medium mt-1">Darmowa wycena</div>
</div>
<button onClick={() => handleAddToCart(true)} className="btn btn-primary text-white">
<ShoppingCart className="w-4 h-4 mr-2" />
Kup z montażem
</button>
</div>
</div>
)}
{/* Cena bez montażu */}
<div className="flex justify-between items-end flex-wrap gap-4">
<div>
<span className="block text-text-muted text-xs font-semibold mb-1 uppercase tracking-wider">Cena urządzenia bez montażu (23% VAT)</span>
<div className="text-xl font-bold text-primary font-heading">{formatCurrency(currentPrice)}</div>
</div>
<button onClick={() => handleAddToCart(false)} className="btn btn-outline btn-primary bg-white">
Do koszyka
</button>
</div>
{/* Feedback o dodaniu */}
{addedMessage && (
<div className="flex items-center gap-2 text-sm font-bold text-green-600 bg-green-50 p-3 rounded-lg border border-green-200 animate-in fade-in slide-in-from-top-2">
<Check className="w-4 h-4" />
{addedMessage}. <button onClick={() => router.push('/koszyk')} className="underline hover:text-green-800">Przejdź do koszyka</button>
</div>
)}
</div>
{/* Atuty */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="flex flex-col items-center text-center p-4 bg-white rounded-xl border border-border">
<Truck className="w-6 h-6 text-accent mb-2" />
<span className="text-xs font-bold text-primary">Darmowa dostawa</span>
<span className="text-[10px] text-text-muted">na terenie Polski</span>
</div>
<div className="flex flex-col items-center text-center p-4 bg-white rounded-xl border border-border">
<ShieldCheck className="w-6 h-6 text-accent mb-2" />
<span className="text-xs font-bold text-primary">5 lat gwarancji</span>
<span className="text-[10px] text-text-muted">na urządzenie i montaż</span>
</div>
<div className="flex flex-col items-center text-center p-4 bg-white rounded-xl border border-border">
<Settings className="w-6 h-6 text-accent mb-2" />
<span className="text-xs font-bold text-primary">Pełne wsparcie</span>
<span className="text-[10px] text-text-muted">na każdym etapie</span>
</div>
</div>
</div>
);
}
+160
View File
@@ -0,0 +1,160 @@
"use client";
import React, { useState, useMemo } from 'react';
import { Product } from '@/types/types';
import { ProductCard } from './ProductCard';
import { SlidersHorizontal, X, Search } from 'lucide-react';
interface ProductsGridProps {
products: Product[];
}
export function ProductsGrid({ products }: ProductsGridProps) {
const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [sortBy, setSortBy] = useState<string>('recommended');
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
// Pobranie unikalnych wartości do filtrów
const brands = useMemo(() => Array.from(new Set(products.map(p => p.brand))), [products]);
const categoryLabels: Record<string, string> = {
'scienne': 'Ścienne',
'multisplit': 'Multisplit',
'kasetonowe': 'Kasetonowe',
'przenosne': 'Przenośne',
};
const categories = useMemo(() => Array.from(new Set(products.map(p => p.category))), [products]);
// Logika filtrowania i sortowania
const filteredProducts = useMemo(() => {
let result = [...products];
if (selectedBrands.length > 0) {
result = result.filter(p => selectedBrands.includes(p.brand));
}
if (selectedCategories.length > 0) {
result = result.filter(p => selectedCategories.includes(p.category));
}
if (sortBy === 'price-asc') {
result.sort((a, b) => a.price - b.price);
} else if (sortBy === 'price-desc') {
result.sort((a, b) => b.price - a.price);
} else if (sortBy === 'power-asc') {
result.sort((a, b) => a.power - b.power);
} else if (sortBy === 'power-desc') {
result.sort((a, b) => b.power - a.power);
}
return result;
}, [products, selectedBrands, selectedCategories, sortBy]);
const toggleBrand = (brand: string) => {
setSelectedBrands(prev =>
prev.includes(brand) ? prev.filter(b => b !== brand) : [...prev, brand]
);
};
const toggleCategory = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category) ? prev.filter(c => c !== category) : [...prev, category]
);
};
return (
<div className="flex flex-col md:flex-row gap-8 py-12">
{/* Mobile Filter Toggle */}
<div className="md:hidden flex justify-between items-center mb-4">
<span className="font-semibold">{filteredProducts.length} wyników</span>
<button className="btn btn-outline btn-primary bg-white" onClick={() => setShowFiltersMobile(!showFiltersMobile)}>
<SlidersHorizontal className="w-4 h-4 mr-2" />
Filtry
</button>
</div>
{/* Sidebar (Filtry) */}
<aside className={`w-full md:w-64 shrink-0 ${showFiltersMobile ? 'block' : 'hidden md:block'}`}>
<div className="bg-white p-6 rounded-2xl border border-border sticky top-24">
<h3 className="font-bold text-lg font-heading text-primary mb-6">Filtrowanie</h3>
<div className="space-y-6">
{/* Marki */}
<div>
<h4 className="font-semibold text-sm mb-3">Marka urządzenia</h4>
<div className="space-y-2">
{brands.map(brand => (
<label key={brand} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedBrands.includes(brand)}
onChange={() => toggleBrand(brand)}
className="rounded border-border text-primary focus:ring-primary"
/>
<span className="text-sm text-text-muted">{brand}</span>
</label>
))}
</div>
</div>
{/* Kategorie */}
<div>
<h4 className="font-semibold text-sm mb-3">Typ urządzenia</h4>
<div className="space-y-2">
{categories.map(category => (
<label key={category} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedCategories.includes(category)}
onChange={() => toggleCategory(category)}
className="rounded border-border text-primary focus:ring-primary"
/>
<span className="text-sm text-text-muted">{categoryLabels[category] || category}</span>
</label>
))}
</div>
</div>
</div>
</div>
</aside>
{/* Główna sekcja (Narzędzia + Siatka) */}
<div className="flex-grow">
{/* Pasek narzędzi */}
<div className="hidden md:flex justify-between items-center bg-white p-4 rounded-xl border border-border mb-8">
<span className="text-text-muted font-medium">{filteredProducts.length} wyników</span>
<div className="flex items-center gap-3">
<label htmlFor="sort" className="text-sm font-semibold text-primary">Sortuj według:</label>
<select
id="sort"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="border border-border rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/20 bg-slate-50"
>
<option value="recommended">Rekomendowane</option>
<option value="price-asc">Cena rosnąco</option>
<option value="price-desc">Cena malejąco</option>
<option value="power-asc">Moc rosnąco</option>
<option value="power-desc">Moc malejąco</option>
</select>
</div>
</div>
{/* Siatka */}
{filteredProducts.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredProducts.map(product => (
<ProductCard key={product.slug} product={product} />
))}
</div>
) : (
<div className="text-center py-20 bg-slate-50 rounded-2xl border border-border">
<h3 className="text-xl font-bold text-primary mb-2">Brak wyników</h3>
<p className="text-text-muted">Spróbuj zmienić kryteria filtrowania.</p>
</div>
)}
</div>
</div>
);
}
+118
View File
@@ -0,0 +1,118 @@
"use client";
import React, { useState } from 'react';
import { Product } from '@/types/types';
import { Star } from 'lucide-react';
interface TabsProps {
product: Product;
}
export function Tabs({ product }: TabsProps) {
const [activeTab, setActiveTab] = useState<'opis' | 'dane' | 'opinie'>('opis');
return (
<div className="mt-16 bg-white rounded-3xl border border-border overflow-hidden">
{/* Nawigacja */}
<div className="flex border-b border-border">
{(['opis', 'dane', 'opinie'] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`flex-1 py-4 text-sm md:text-base font-bold uppercase tracking-wider transition-colors ${
activeTab === tab
? 'bg-slate-50 text-primary border-b-2 border-primary'
: 'bg-white text-text-muted hover:bg-slate-50 hover:text-primary'
}`}
>
{tab === 'opis' && 'Opis produktu'}
{tab === 'dane' && 'Dane techniczne'}
{tab === 'opinie' && 'Opinie'}
</button>
))}
</div>
{/* Zawartość */}
<div className="p-8 md:p-12">
{/* Opis */}
{activeTab === 'opis' && (
<div className="prose prose-slate max-w-none">
<h3 className="text-2xl font-bold text-primary font-heading mb-4">Szczegółowe informacje</h3>
<p className="text-lg text-text-muted leading-relaxed">
{product.description}
</p>
{product.heating && (
<p className="mt-4 font-semibold text-accent">
Ten model posiada funkcję efektywnego ogrzewania, dzięki czemu sprawdzi się doskonale jako dodatkowe źródło ciepła w okresach przejściowych lub główny system grzewczy dla wybranych pomieszczeń.
</p>
)}
</div>
)}
{/* Dane techniczne */}
{activeTab === 'dane' && (
<div>
<h3 className="text-2xl font-bold text-primary font-heading mb-6">Specyfikacja techniczna</h3>
{product.specs ? (
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<tbody>
{Object.entries(product.specs).map(([key, value], idx) => (
<tr key={idx} className="border-b border-border hover:bg-slate-50 transition-colors">
<th className="py-4 px-4 font-semibold text-primary w-1/2">{key}</th>
<td className="py-4 px-4 text-text-muted w-1/2">{value as React.ReactNode}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-text-muted">Brak szczegółowych danych technicznych dla tego modelu.</p>
)}
</div>
)}
{/* Opinie */}
{activeTab === 'opinie' && (
<div>
<div className="flex items-center gap-4 mb-8">
<div className="text-4xl font-extrabold text-primary font-heading">4.9</div>
<div className="flex flex-col">
<div className="flex text-yellow-400">
<Star className="w-5 h-5 fill-current" />
<Star className="w-5 h-5 fill-current" />
<Star className="w-5 h-5 fill-current" />
<Star className="w-5 h-5 fill-current" />
<Star className="w-5 h-5 fill-current" />
</div>
<span className="text-sm text-text-muted font-medium mt-1">Na podstawie 12 opinii</span>
</div>
</div>
<div className="space-y-6">
{/* Przykładowa opinia */}
<div className="p-6 bg-slate-50 rounded-2xl border border-border">
<div className="flex justify-between items-start mb-3">
<div>
<span className="font-bold text-primary">Janusz z Wrocławia</span>
<span className="text-xs text-text-muted block mt-0.5">Potwierdzony zakup z montażem</span>
</div>
<div className="flex text-yellow-400">
<Star className="w-4 h-4 fill-current" />
<Star className="w-4 h-4 fill-current" />
<Star className="w-4 h-4 fill-current" />
<Star className="w-4 h-4 fill-current" />
<Star className="w-4 h-4 fill-current" />
</div>
</div>
<p className="text-text-muted text-sm leading-relaxed">
Sprzęt najwyższej klasy. Bardzo cichy, w ogóle go nie słychać w sypialni. Montaż wykonany przez panów z AirProject przebiegł super szybko i czyściutko. Polecam!
</p>
</div>
</div>
</div>
)}
</div>
</div>
);
}
+33
View File
@@ -0,0 +1,33 @@
// TODO: Dane firmy — do uzupełnienia przez właściciela
export const companyDetails = {
name: "AeroService HVAC & Usługi Domowe",
shortName: "AeroService",
nip: "1234567890", // TODO: wstaw realny NIP
phone: "+48 123 456 789",
phoneRaw: "+48123456789",
email: "[email protected]",
address: {
street: "ul. Klimatyczna 15",
zipCode: "50-001",
city: "Wrocław",
region: "Dolny Śląsk",
},
hours: "Pon - Pt: 8:00 - 18:00, Sob: 9:00 - 14:00",
socials: {
facebook: "https://facebook.com",
instagram: "https://instagram.com",
google: "https://google.com/maps",
},
stats: [
{ label: "Lat doświadczenia", value: "15+", number: 15 },
{ label: "Ukończonych realizacji", value: "3500+", number: 3500 },
{ label: "Zadowolonych klientów", value: "99%", number: 99 },
{ label: "Obsługiwanych miast", value: "12+", number: 12 },
],
certificates: [
{ name: "Certyfikat F-Gaz dla przedsiębiorcy", image: "/placeholder-fgaz.png" },
{ name: "Autoryzowany Instalator Daikin", image: "/placeholder-daikin.png" },
{ name: "Autoryzowany Instalator Mitsubishi Electric", image: "/placeholder-mitsubishi.png" },
{ name: "Certyfikat Rzetelna Firma", image: "/placeholder-rzetelna.png" },
],
};
+102
View File
@@ -0,0 +1,102 @@
export interface FaqItem {
id: string;
question: string;
answer: string;
category: 'klimatyzacja' | 'pompy-ciepla' | 'remonty' | 'hydraulika' | 'serwis-agd';
}
export const faqData: FaqItem[] = [
// Klimatyzacja
{
id: 'klimatyzacja-moc',
question: 'Jak dobrać odpowiednią moc klimatyzatora?',
answer: 'Standardowo przyjmuje się, że na każde 10 m² powierzchni pomieszczenia potrzeba około 1 kW mocy chłodniczej. Dla pokoju o powierzchni 25 m² odpowiedni będzie klimatyzator o mocy 2.5 kW. Należy jednak wziąć pod uwagę również nasłonecznienie, izolację budynku oraz liczbę urządzeń emitujących ciepło.',
category: 'klimatyzacja',
},
{
id: 'klimatyzacja-czas',
question: 'Ile czasu trwa standardowy montaż?',
answer: 'Standardowy montaż klimatyzacji typu split w mieszkaniu lub domu trwa zazwyczaj od 4 do 8 godzin. Czas ten może się wydłużyć w przypadku bardziej skomplikowanych instalacji, np. długich tras rurociągów lub konieczności użycia podnośnika koszowego.',
category: 'klimatyzacja',
},
{
id: 'klimatyzacja-wspolnota',
question: 'Czy potrzebuję zgody spółdzielni lub wspólnoty mieszkaniowej?',
answer: 'Tak, montaż jednostki zewnętrznej na elewacji budynku (części wspólnej) wymaga uzyskania zgody administratora, spółdzielni lub wspólnoty. W przypadku montażu na posadzce balkonu, który jest przypisany do mieszkania, często zgoda nie jest wymagana, jednak warto to potwierdzić w regulaminie.',
category: 'klimatyzacja',
},
{
id: 'klimatyzacja-vat',
question: 'Kiedy obowiązuje stawka VAT 8%, a kiedy 23%?',
answer: 'Stawka VAT 8% dotyczy usługi montażu wraz z urządzeniem w obiektach budowlanych zaliczanych do budownictwa objętego społecznym programem mieszkaniowym (np. domy jednorodzinne do 300 m², mieszkania do 150 m²). Dla firm oraz w obiektach o większym metrażu obowiązuje stawka VAT 23%.',
category: 'klimatyzacja',
},
{
id: 'klimatyzacja-przeglad',
question: 'Jak często należy wykonywać przegląd i odgrzybianie klimatyzacji?',
answer: 'Zalecamy wykonywanie przeglądu i odgrzybiania co najmniej 2 razy w roku – przed sezonem letnim (chłodzenie) oraz przed sezonem zimowym (grzanie). Regularny serwis zapewnia czyste powietrze, dłuższą żywotność urządzenia oraz zachowanie gwarancji.',
category: 'klimatyzacja',
},
{
id: 'klimatyzacja-grzanie',
question: 'Czy klimatyzatorem można ogrzewać pomieszczenia?',
answer: 'Tak, niemal wszystkie nowoczesne klimatyzatory to w rzeczywistości pompy ciepła typu powietrze-powietrze, które doskonale sprawdzają się jako źródło ogrzewania, zwłaszcza w okresach przejściowych. Wiele modeli efektywnie grzeje nawet przy temperaturach zewnętrznych rzędu -15°C czy -25°C.',
category: 'klimatyzacja',
},
{
id: 'klimatyzacja-samodzielnie',
question: 'Czy mogę zamontować klimatyzację samodzielnie?',
answer: 'Zgodnie z prawem, montaż klimatyzacji zawierającej czynniki chłodnicze F-gazowe może być wykonany wyłącznie przez osobę lub firmę posiadającą odpowiednie certyfikaty UDT. Ponadto samodzielny montaż skutkuje utratą gwarancji producenta urządzenia.',
category: 'klimatyzacja',
},
// Pompy ciepła
{
id: 'pompy-koszty',
question: 'Jakie są koszty inwestycji w pompę ciepła?',
answer: 'Koszt inwestycji zależy od rodzaju pompy ciepła, jej mocy, zapotrzebowania budynku na ciepło oraz konieczności modernizacji instalacji. Ceny zaczynają się od około 25 000 zł do nawet 50 000 zł i więcej, jednak znaczną część tych kosztów można pokryć z dotacji.',
category: 'pompy-ciepla',
},
{
id: 'pompy-zima',
question: 'Czy pompa ciepła poradzi sobie z ogrzewaniem w bardzo mroźne zimy?',
answer: 'Nowoczesne pompy ciepła są zaprojektowane do pracy w polskich warunkach klimatycznych. Bez problemu ogrzewają budynki nawet przy temperaturach rzędu -20°C czy -25°C. W skrajnie niskich temperaturach mogą wspomagać się wbudowaną grzałką elektryczną.',
category: 'pompy-ciepla',
},
{
id: 'pompy-halas',
question: 'Czy pompy ciepła są głośne?',
answer: 'Nowoczesne urządzenia są bardzo ciche i nie powodują dyskomfortu dla mieszkańców ani sąsiadów. Poziom hałasu jednostki zewnętrznej to zazwyczaj od 40 do 55 dB w odległości kilku metrów (zbliżone do głośności zwykłej rozmowy lub szumu lodówki).',
category: 'pompy-ciepla',
},
// Remonty
{
id: 'remonty-wycena',
question: 'Ile kosztuje niezobowiązująca wycena remontu?',
answer: 'Dojazd i przygotowanie kosztorysu remontu na terenie naszego miasta i bliskich okolic są bezpłatne. Na spotkaniu ocenimy stan faktyczny, wymierzymy pomieszczenia i omówimy zakres prac.',
category: 'remonty',
},
{
id: 'remonty-materialy',
question: 'Kto kupuje materiały budowlane?',
answer: 'Możemy pracować na materiałach powierzonych przez klienta lub wziąć na siebie ich zakup i transport (często korzystamy wtedy ze zniżek w hurtowniach). Sposób rozliczenia ustalamy indywidualnie.',
category: 'remonty',
},
// Hydraulika
{
id: 'hydraulika-awaria',
question: 'Czy przyjeżdżacie do awarii hydraulicznych w weekendy?',
answer: 'Świadczymy usługi pogotowia hydraulicznego. W przypadku pilnych awarii prosimy o kontakt pod podany numer telefonu alarmowego – staramy się reagować najszybciej jak to możliwe.',
category: 'hydraulika',
},
// Serwis AGD
{
id: 'agd-dojazd',
question: 'Czy naprawiacie sprzęt AGD w domu klienta?',
answer: 'Większość napraw (ok. 90%) realizujemy bezpośrednio w domu klienta na pierwszej lub drugiej wizycie. Tylko w nielicznych przypadkach, wymagających skomplikowanej diagnozy lub specjalistycznego sprzętu, zabieramy urządzenie do warsztatu.',
category: 'serwis-agd',
},
];
+69
View File
@@ -0,0 +1,69 @@
import { Post } from "@/types/types";
// MOCK
export const posts: Post[] = [
{
slug: "jaki-klimatyzator-wybrac-do-mieszkania",
title: "Jaki klimatyzator wybrać do mieszkania? Split vs. Multisplit",
category: "klimatyzacja",
excerpt: "Stoisz przed wyborem klimatyzatora do swojego mieszkania? Poznaj różnice między systemami split i multisplit oraz dowiedz się, jaka moc będzie odpowiednia.",
date: "2026-06-15T12:00:00.000Z",
author: "Tomasz Kowalski",
image: "/blog/post-1.png",
content: `<p>Wybór odpowiedniego klimatyzatora do mieszkania to kluczowa decyzja, która wpływa na komfort domowników przez wiele lat. W tym poradniku przyjrzymy się dwóm najpopularniejszym typom instalacji chłodniczych: systemom split oraz multisplit.</p>
<h2>Co to jest klimatyzator typu split?</h2>
<p>System typu split składa się z jednej jednostki wewnętrznej (montowanej w pokoju) oraz jednej jednostki zewnętrznej (agregatu). To najbardziej popularne i ekonomiczne rozwiązanie, idealne do schłodzenia jednego, głównego pomieszczenia w mieszkaniu, np. salonu.</p>
<h2>System multisplit — kiedy warto dopłacić?</h2>
<p>Jeśli planujesz klimatyzować kilka pokoi (np. salon, sypialnię oraz pokój dziecka), system multisplit będzie lepszym wyborem. Umożliwia on podłączenie do 5 jednostek wewnętrznych do jednego agregatu zewnętrznego. Pozwala to zaoszczędzić miejsce na balkonie czy elewacji i niezależnie sterować temperaturą w każdym pokoju.</p>
<h2>Jak dobrać moc klimatyzatora?</h2>
<p>Podstawowy przelicznik to 1 kW mocy chłodniczej na każde 10 m² powierzchni pomieszczenia. Dla pokoju o powierzchni 25 m² idealnym wyborem będzie urządzenie o mocy chłodniczej 2.5 kW, a dla 35 m² – model o mocy 3.5 kW.</p>`,
featured: true,
},
{
slug: "dofinansowanie-do-pomp-ciepla-2026",
title: "Dofinansowanie do pomp ciepła w 2026 roku. Jak zyskać do 100% dotacji?",
category: "pompy-ciepla",
excerpt: "Sprawdź aktualne zasady programów Czyste Powietrze, Mój Prąd oraz ulgi termomodernizacyjnej w 2026 roku. Dowiedz się, jak złożyć wniosek.",
date: "2026-06-10T10:00:00.000Z",
author: "Janusz Nowak",
image: "/blog/post-2.png",
content: `<p>Inwestycja w pompę ciepła to doskonały sposób na obniżenie rachunków za ogrzewanie i dbanie o środowisko. Dzięki rządowym dotacjom, koszty zakupu i montażu mogą zostać znacząco obniżone. Jak uzyskać dotację w 2026 roku?</p>
<h2>Program Czyste Powietrze</h2>
<p>W 2026 roku program Czyste Powietrze wciąż oferuje najwyższe kwoty dofinansowania dla osób wymieniających stare kotły stałopalne na nowoczesne pompy ciepła. W zależności od dochodów gospodarstwa domowego, dotacja może wynieść nawet do 100% kosztów kwalifikowanych.</p>
<h2>Mój Prąd 8.0</h2>
<p>Dla osób montujących mikroinstalacje fotowoltaiczne wraz z pompą ciepła, program Mój Prąd oferuje dodatkowe dotacje na magazyny energii oraz urządzenia grzewcze. Łączenie tych technologii daje największe oszczędności.</p>`,
},
{
slug: "remont-lazienki-krok-po-kroku",
title: "Remont łazienki krok po kroku — planowanie i etapy prac",
category: "remonty",
excerpt: "Remont łazienki bywa wyzwaniem. Zobacz nasz harmonogram prac wykończeniowych, aby uniknąć błędów i niepotrzebnych kosztów w trakcie realizacji.",
date: "2026-06-05T09:00:00.000Z",
author: "Michał Wiśniewski",
image: "/blog/post-3.png",
content: `<p>Łazienka to jedno z najbardziej wymagających pomieszczeń do wykończenia. Aby remont przebiegł sprawnie i bez niespodzianek, warto trzymać się ustalonego planu prac.</p>
<h2>Krok 1: Projekt i instalacje</h2>
<p>Zanim położysz pierwsze płytki, upewnij się, że instalacje hydrauliczne i elektryczne są dostosowane do nowego układu sanitariatów. Zmiana umiejscowienia wanny czy pralki wymaga wcześniejszego przerobienia podejść wod-kan.</p>
<h2>Krok 2: Hydroizolacja</h2>
<p>To kluczowy etap remontu łazienki. Hydroizolacja (tzw. płynna folia) chroni ściany i stropy przed wilgocią. Należy ją bezwzględnie zaaplikować w strefach mokrych: wokół brodzika, wanny i umywalki.</p>`,
},
{
slug: "jak-czesto-robic-przeglad-klimatyzacji",
title: "Jak często robić przegląd klimatyzacji w domu?",
category: "porady",
excerpt: "Zastanawiasz się, dlaczego regularny serwis klimatyzacji jest tak ważny? Sprawdź zalecenia i dowiedz się, kiedy najlepiej wezwać serwisanta.",
date: "2026-05-28T08:00:00.000Z",
author: "Tomasz Kowalski",
image: "/blog/post-4.png",
content: `<p>Regularne przeglądy klimatyzacji domowej są niezbędne nie tylko do zachowania gwarancji producenta, ale przede wszystkim dla zdrowia domowników i wydajnej pracy urządzenia.</p>
<h2>Zalecana częstotliwość przeglądów</h2>
<p>Standardowo serwis klimatyzacji powinien być przeprowadzany 2 razy w roku: wiosną (przed sezonem chłodniczym) oraz jesienią (po intensywnym chłodzeniu lub przed sezonem grzewczym).</p>`,
},
];
export const getPosts = () => posts;
export const getPostBySlug = (slug: string) => posts.find((p) => p.slug === slug);
export const getLatestPosts = (limit = 3) => [...posts].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, limit);
export const getFeaturedPost = () => posts.find((p) => p.featured) || posts[0];
export const getRelatedPosts = (currentSlug: string, category: string, limit = 3) =>
posts.filter((p) => p.slug !== currentSlug && p.category === category).slice(0, limit);
+67
View File
@@ -0,0 +1,67 @@
export interface PricingItem {
id: string;
name: string;
priceNet: number;
priceGrossPrivate: number; // 8% VAT
priceGrossCompany: number; // 23% VAT
category: 'split' | 'multisplit' | 'additional';
}
export const pricingData: PricingItem[] = [
{
id: 'montaz-split-podstawowy',
name: 'Montaż klimatyzacji typu SPLIT (do 3.5 kW)',
priceNet: 1200,
priceGrossPrivate: 1296,
priceGrossCompany: 1476,
category: 'split',
},
{
id: 'montaz-split-rozszerzony',
name: 'Montaż klimatyzacji typu SPLIT (powyżej 3.5 kW)',
priceNet: 1500,
priceGrossPrivate: 1620,
priceGrossCompany: 1845,
category: 'split',
},
{
id: 'montaz-multisplit-2',
name: 'Montaż klimatyzacji MULTISPLIT (2 jednostki wew.)',
priceNet: 2200,
priceGrossPrivate: 2376,
priceGrossCompany: 2706,
category: 'multisplit',
},
{
id: 'montaz-multisplit-3',
name: 'Montaż klimatyzacji MULTISPLIT (3 jednostki wew.)',
priceNet: 3000,
priceGrossPrivate: 3240,
priceGrossCompany: 3690,
category: 'multisplit',
},
{
id: 'dodatkowy-metr',
name: 'Dodatkowy metr instalacji chłodniczej',
priceNet: 100,
priceGrossPrivate: 108,
priceGrossCompany: 123,
category: 'additional',
},
{
id: 'kucie-bruzdy',
name: 'Kucie bruzdy w betonie/żelbecie (mb)',
priceNet: 150,
priceGrossPrivate: 162,
priceGrossCompany: 184.5,
category: 'additional',
},
{
id: 'przeglad-klimatyzacji',
name: 'Okresowy przegląd i odgrzybianie klimatyzacji',
priceNet: 250,
priceGrossPrivate: 270,
priceGrossCompany: 307.5,
category: 'additional',
},
];
+136
View File
@@ -0,0 +1,136 @@
import { ProcessStep } from "@/types/types";
// MOCK
export const mainProcessSteps: ProcessStep[] = [
{
number: 1,
title: "Kontakt i konsultacja",
description: "Zadzwoń do nas lub wypełnij formularz. Omówimy Twoje potrzeby i wstępne pomysły.",
icon: "PhoneCall",
},
{
number: 2,
title: "Audyt i wycena",
description: "Przyjedziemy na bezpłatną wizję lokalną, dobierzemy moc urządzeń i przygotujemy kosztorys.",
icon: "ClipboardCheck",
},
{
number: 3,
title: "Profesjonalny montaż",
description: "Nasza wykwalifikowana ekipa sprawnie i czysto wykona zaplanowane prace instalacyjne lub remontowe.",
icon: "Cpu",
},
{
number: 4,
title: "Uruchomienie i serwis",
description: "Uruchomimy system, przeszkolimy Cię z obsługi i zapewnimy opiekę gwarancyjną oraz pogwarancyjną.",
icon: "HeartHandshake",
},
];
export const acProcessSteps: ProcessStep[] = [
{
number: 1,
title: "Zabezpieczenie miejsca",
description: "Zabezpieczamy meble i podłogi folią malarską przed rozpoczęciem jakichkolwiek prac.",
icon: "ShieldAlert",
},
{
number: 2,
title: "Przewiert przez ścianę",
description: "Wykonujemy precyzyjny przewiert o odpowiednim kącie na instalację freonową i odpływ skroplin.",
icon: "Construction",
},
{
number: 3,
title: "Prowadzenie instalacji",
description: "Układamy rury miedziane w otulinie, kable sterujące oraz odpływ skroplin w korytkach lub bruzdach.",
icon: "Layers",
},
{
number: 4,
title: "Montaż jednostki wewn.",
description: "Instalujemy stelaż i wieszamy jednostkę wewnętrzną na odpowiedniej wysokości.",
icon: "MonitorDown",
},
{
number: 5,
title: "Montaż jednostki zewn.",
description: "Mocujemy agregat zewnętrzny na wspornikach ściennych, stojaku lub balkonie.",
icon: "Server",
},
{
number: 6,
title: "Próba szczelności i próżnia",
description: "Wykonujemy próbę ciśnieniową azotem, a następnie wytwarzamy próżnię w instalacji freonowej.",
icon: "Gauge",
},
{
number: 7,
title: "Uruchomienie i testy",
description: "Wpuszczamy czynnik chłodniczy R32, uruchamiamy klimatyzator i mierzymy parametry pracy.",
icon: "Zap",
},
{
number: 8,
title: "Szkolenie i przekazanie",
description: "Tłumaczymy zasady działania pilota/aplikacji oraz przekazujemy kartę gwarancyjną.",
icon: "BookOpen",
},
];
export const heatPumpProcessSteps: ProcessStep[] = [
{
number: 1,
title: "Audyt energetyczny",
description: "Analiza zapotrzebowania budynku na ciepło, stanu izolacji oraz istniejącej instalacji grzewczej.",
},
{
number: 2,
title: "Dobór urządzenia",
description: "Wybór optymalnej mocy pompy ciepła oraz zbiorników buforowych i C.W.U.",
},
{
number: 3,
title: "Pomoc w dotacjach",
description: "Kompletowanie dokumentacji do programów Czyste Powietrze, Mój Prąd lub ulgi termomodernizacyjnej.",
},
{
number: 4,
title: "Montaż kotłowni i agregatu",
description: "Prace hydrauliczne i elektryczne wewnątrz budynku oraz instalacja agregatu zewnętrznego na fundamencie.",
},
{
number: 5,
title: "Pierwsze uruchomienie",
description: "Konfiguracja krzywej grzewczej, odpowietrzenie układu i testy bezpieczeństwa.",
},
{
number: 6,
title: "Stała opieka serwisowa",
description: "Coroczne przeglądy gwarancyjne gwarantujące bezawaryjną i tanią pracę pompy przez lata.",
},
];
export const homeProcessSteps: ProcessStep[] = [
{
number: 1,
title: "Zgłoszenie i kontakt",
description: "Opisz zakres prac lub zgłoś usterkę telefonicznie bądź przez formularz.",
},
{
number: 2,
title: "Wycena prac i materiałów",
description: "Określamy orientacyjny kosztorys prac oraz doradzamy przy doborze materiałów wykończeniowych.",
},
{
number: 3,
title: "Terminowa realizacja",
description: "Wchodzimy na budowę w umówionym terminie. Dbamy o porządek i przestrzeganie norm budowlanych.",
},
{
number: 4,
title: "Odbiór i gwarancja",
description: "Wspólnie odbieramy wykonane prace. Na wszystkie usługi remontowo-instalacyjne udzielamy gwarancji.",
},
];
+178
View File
@@ -0,0 +1,178 @@
import { Product } from "@/types/types";
// MOCK
export const products: Product[] = [
{
slug: "rotenso-imera-3-5kw",
name: "Imera 3.5kW",
brand: "Rotenso",
category: "scienne",
power: 3.5,
price: 3499,
priceWithInstall: 4799,
image: "https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=800&q=80",
features: ["Wi-Fi", "Jonizator", "Grzanie do -22°C", "Pozłacane lamele"],
heating: true,
description: "Klimatyzator ścienny Rotenso Imera cechuje się eleganckim wyglądem i wysoką energooszczędnością A+++. Wyposażony jest w zaawansowane filtry powietrza.",
specs: {
"Klasa energetyczna chłodzenie/grzanie": "A+++ / A++",
"Pobór mocy chłodzenie (W)": "950",
"Poziom hałasu min/max (dB)": "20 / 38",
"Zakres temperatur zewnętrznych - chłodzenie": "-15°C do 50°C",
"Zakres temperatur zewnętrznych - grzanie": "-22°C do 24°C",
"Czynnik chłodniczy": "R32",
},
variants: [
{ label: "2.6 kW", power: 2.6, price: 3199, priceWithInstall: 4399 },
{ label: "3.5 kW", power: 3.5, price: 3499, priceWithInstall: 4799 },
{ label: "5.3 kW", power: 5.3, price: 4599, priceWithInstall: 6199 },
],
},
{
slug: "daikin-sensira-3-5kw",
name: "Sensira 3.5kW",
brand: "Daikin",
category: "scienne",
power: 3.5,
price: 3999,
priceWithInstall: 5299,
image: "https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80",
features: ["Cicha praca 21dB", "Sterowanie Online (opcja)", "Klasa A++", "Tryb komfortowy"],
heating: true,
description: "Klimatyzator Daikin Sensira zapewnia wysoki komfort i cichą pracę. To idealny wybór dla osób szukających japońskiej niezawodności w rozsądnej cenie.",
specs: {
"Klasa energetyczna chłodzenie/grzanie": "A++ / A+",
"Pobór mocy chłodzenie (W)": "1050",
"Poziom hałasu min/max (dB)": "21 / 40",
"Zakres temperatur zewnętrznych - chłodzenie": "-10°C do 46°C",
"Zakres temperatur zewnętrznych - grzanie": "-15°C do 18°C",
"Czynnik chłodniczy": "R32",
},
variants: [
{ label: "2.5 kW", power: 2.5, price: 3599, priceWithInstall: 4899 },
{ label: "3.5 kW", power: 3.5, price: 3999, priceWithInstall: 5299 },
{ label: "5.0 kW", power: 5.0, price: 5299, priceWithInstall: 6899 },
],
},
{
slug: "mitsubishi-hr-3-5kw",
name: "M-Series HR 3.5kW",
brand: "Mitsubishi Electric",
category: "scienne",
power: 3.5,
price: 4299,
priceWithInstall: 5599,
image: "https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=800&q=80",
features: ["Kompaktowa budowa", "Filtr z jonami srebra", "Wi-Fi (opcja)", "Klasa A++"],
heating: true,
description: "Klimatyzatory ścienne Mitsubishi z serii HR to harmonijne połączenie nowoczesnego designu z optymalną wydajnością energetyczną i cichym działaniem.",
specs: {
"Klasa energetyczna chłodzenie/grzanie": "A++ / A+",
"Pobór mocy chłodzenie (W)": "1010",
"Poziom hałasu min/max (dB)": "21 / 43",
"Zakres temperatur zewnętrznych - chłodzenie": "-10°C do 46°C",
"Czynnik chłodniczy": "R32",
},
},
{
slug: "gree-pular-3-2kw",
name: "Pular 3.2kW",
brand: "Gree",
category: "scienne",
power: 3.2,
price: 2899,
priceWithInstall: 4199,
image: "https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80",
features: ["Wi-Fi", "Jonizator plasma", "Automatyczne czyszczenie", "7 biegów wentylatora"],
heating: true,
description: "Gree Pular to jeden z najchętniej wybieranych klimatyzatorów ze względu na doskonały stosunek ceny do jakości, cichą pracę oraz rozbudowane funkcje.",
specs: {
"Klasa energetyczna chłodzenie/grzanie": "A++ / A+",
"Pobór mocy chłodzenie (W)": "990",
"Poziom hałasu min/max (dB)": "24 / 37",
"Zakres temperatur zewnętrznych - chłodzenie": "-15°C do 43°C",
"Czynnik chłodniczy": "R32",
},
variants: [
{ label: "2.7 kW", power: 2.7, price: 2699, priceWithInstall: 3999 },
{ label: "3.2 kW", power: 3.2, price: 2899, priceWithInstall: 4199 },
{ label: "4.6 kW", power: 4.6, price: 3899, priceWithInstall: 5299 },
],
},
{
slug: "lg-standard-plus-3-5kw",
name: "Standard Plus 3.5kW",
brand: "LG",
category: "scienne",
power: 3.5,
price: 3699,
priceWithInstall: 4999,
image: "https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=800&q=80",
features: ["Wi-Fi ThinQ", "Dual Inverter", "Aktywna kontrola zużycia energii", "Cicha praca"],
heating: true,
description: "Dzięki sprężarce Dual Inverter klimatyzator LG Standard Plus zapewnia wyjątkowo szybkie chłodzenie oraz oszczędność energii przy cichym działaniu.",
specs: {
"Klasa energetyczna chłodzenie/grzanie": "A++ / A+",
"Czynnik chłodniczy": "R32",
},
},
{
slug: "gree-multisplit-2x2-5kw",
name: "Multisplit Gree 2x2.5kW",
brand: "Gree",
category: "multisplit",
power: 5.2,
price: 6499,
priceWithInstall: 8499,
image: "https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80",
features: ["2 jednostki wewnętrzne", "Niezależne sterowanie", "Wi-Fi", "Klasa A++"],
heating: true,
description: "System klimatyzacji Multisplit Gree składający się z jednego agregatu zewnętrznego i dwóch jednostek ściennych, idealny do chłodzenia dwóch sąsiednich pokoi.",
specs: {
"Maksymalna liczba jednostek wewn.": "2",
"Moc agregatu (kW)": "5.2",
"Czynnik chłodniczy": "R32",
},
},
{
slug: "aux-kasetonowy-5-0kw",
name: "Kasetonowy Aux 5.0kW",
brand: "Aux",
category: "kasetonowe",
power: 5.0,
price: 5499,
priceWithInstall: 7299,
image: "https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=800&q=80",
features: ["Nawiew 360°", "Pompka skroplin", "Wbudowane Wi-Fi", "Kompaktowa obudowa"],
heating: true,
description: "Klimatyzator kasetonowy marki Aux doskonale sprawdzi się w pomieszczeniach biurowych, handlowych i usługowych z sufitami podwieszanymi.",
specs: {
"Moc chłodzenia (kW)": "5.0",
"Klasa energetyczna chłodzenie/grzanie": "A++ / A+",
"Czynnik chłodniczy": "R32",
},
},
{
slug: "rotenso-oros-przenosny-3-5kw",
name: "Oros Przenośny 3.5kW",
brand: "Rotenso",
category: "przenosne",
power: 3.5,
price: 1899,
priceWithInstall: 1899, // brak montażu ściennego
image: "https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80",
features: ["Brak stałego montażu", "Dotykowy panel", "Ekologiczny czynnik", "Kółka transportowe"],
heating: false,
description: "Klimatyzator przenośny Rotenso Oros to doskonałe rozwiązanie w miejscach, gdzie montaż jednostki ściennej (zewnętrznej) jest niemożliwy.",
specs: {
"Klasa energetyczna chłodzenie": "A",
"Pobór mocy chłodzenie (W)": "1350",
"Poziom hałasu (dB)": "65",
"Czynnik chłodniczy": "R290",
},
},
];
export const getProducts = () => products;
export const getProductBySlug = (slug: string) => products.find((p) => p.slug === slug);
export const getFeaturedProducts = (limit = 4) => products.slice(0, limit);
+59
View File
@@ -0,0 +1,59 @@
export interface Project {
id: string;
title: string;
description: string;
category: 'klimatyzacja' | 'pompy-ciepla' | 'remonty' | 'hydraulika';
location: string;
imageUrl: string;
beforeAfter?: {
beforeUrl: string;
afterUrl: string;
};
}
export const projectsData: Project[] = [
{
id: 'klima-dom-jednorodzinny',
title: 'Montaż klimatyzacji w domu jednorodzinnym',
description: 'Instalacja systemu multisplit dla 4 pomieszczeń. Ukryte prowadzenie instalacji chłodniczej.',
category: 'klimatyzacja',
location: 'Wrocław, Krzyki',
imageUrl: 'https://images.unsplash.com/photo-1527689638836-411945a2b57c?w=800&q=80',
},
{
id: 'klima-biuro',
title: 'Klimatyzacja biura',
description: 'Montaż wydajnych jednostek kasetonowych w przestrzeni biurowej o powierzchni 150m2.',
category: 'klimatyzacja',
location: 'Wrocław, Fabryczna',
imageUrl: 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&q=80',
},
{
id: 'pompa-ciepla-modernizacja',
title: 'Wymiana pieca węglowego na pompę ciepła',
description: 'Modernizacja kotłowni i montaż pompy ciepła typu powietrze-woda o mocy 12kW.',
category: 'pompy-ciepla',
location: 'Długołęka',
imageUrl: 'https://images.unsplash.com/photo-1584622650111-993a426fbf0a?w=800&q=80',
},
{
id: 'remont-lazienki',
title: 'Kompleksowy remont łazienki',
description: 'Wymiana instalacji hydraulicznej, hydroizolacja, położenie nowoczesnych płytek wielkoformatowych.',
category: 'remonty',
location: 'Wrocław, Śródmieście',
imageUrl: 'https://images.unsplash.com/photo-1552321554-5fefe8c9ef14?w=800&q=80',
beforeAfter: {
beforeUrl: 'https://images.unsplash.com/photo-1584622781564-1d987f7333c1?w=800&q=80', // Placeholder
afterUrl: 'https://images.unsplash.com/photo-1552321554-5fefe8c9ef14?w=800&q=80'
}
},
{
id: 'hydraulika-nowa-instalacja',
title: 'Nowa instalacja wod-kan',
description: 'Rozprowadzenie instalacji wodno-kanalizacyjnej w nowo budowanym domu jednorodzinnym.',
category: 'hydraulika',
location: 'Siechnice',
imageUrl: 'https://images.unsplash.com/photo-1585704032915-c3400ca199e7?w=800&q=80',
}
];
+41
View File
@@ -0,0 +1,41 @@
import { ServiceItem } from "@/types/types";
// MOCK
export const services: ServiceItem[] = [
{
title: "Montaż klimatyzacji",
description: "Profesjonalny dobór, montaż klimatyzacji split i multisplit dla domów, mieszkań i firm.",
icon: "Wind",
href: "/klimatyzacja",
},
{
title: "Serwis i przeglądy",
description: "Regularne przeglądy, czyszczenie, odgrzybianie oraz serwis naprawczy urządzeń klimatyzacyjnych.",
icon: "Wrench",
href: "/klimatyzacja",
},
{
title: "Pompy ciepła",
description: "Dobór i montaż ekologicznych pomp ciepła powietrze-woda. Pomoc w uzyskaniu dofinansowania.",
icon: "ThermometerSun",
href: "/pompy-ciepla",
},
{
title: "Remonty i wykończenia",
description: "Kompleksowe usługi remontowe mieszkań, łazienek i kuchni. Prace gładziarskie, glazurnicze i malowanie.",
icon: "Paintbrush",
href: "/uslugi-domowe/remonty",
},
{
title: "Usługi hydrauliczne",
description: "Instalacje wodno-kanalizacyjne, montaż armatury (biały montaż) oraz szybka pomoc przy awariach.",
icon: "Droplet",
href: "/uslugi-domowe/hydraulika",
},
{
title: "Serwis lodówek i AGD",
description: "Naprawa lodówek, zamrażarek oraz innych domowych urządzeń chłodniczych z dojazdem do klienta.",
icon: "Tv",
href: "/uslugi-domowe/serwis-agd",
},
];
+26
View File
@@ -0,0 +1,26 @@
import { Testimonial } from "@/types/types";
// MOCK
export const testimonials: Testimonial[] = [
{
name: "Mariusz Wójcik",
rating: 5,
text: "Firma montowała u mnie klimatyzację multisplit (3 jednostki wewnętrzne). Pełen profesjonalizm od wyceny do sprzątania po montażu. Rury ukryte perfekcyjnie, sprzęt działa cicho i wydajnie. Polecam!",
source: "Google Reviews",
date: "2026-05-20",
},
{
name: "Anna Kowalczyk",
rating: 5,
text: "Skorzystałam z usługi wykończenia łazienki oraz prac hydraulicznych. Wszystko przebiegło zgodnie z umówionym kosztorysem i harmonogramem. Bardzo czysta praca i dbałość o detale. Zdecydowanie warci polecenia fachowcy.",
source: "Google Reviews",
date: "2026-06-02",
},
{
name: "Grzegorz Kamiński",
rating: 5,
text: "Kompleksowo: pompa ciepła z dofinansowaniem plus doradztwo. Pomogli wypełnić wszystkie dokumenty Czystego Powietrza, a sam montaż trwał tylko dwa dni. Bardzo dziękuję za świetny kontakt i profesjonalne wdrożenie.",
source: "Google Reviews",
date: "2026-06-12",
},
];
+58
View File
@@ -0,0 +1,58 @@
# Strona 01 — Strona główna (`/`)
Cel: w kilka sekund pokazać dwa filary (HVAC + usługi domowe) i skierować klienta do jego tematu.
Komponenty z `components/sections/`, dane z `content/`.
---
## Sekcje (kolejność od góry)
### 1. Hero (`HeroSection`)
- H1: np. „Klimatyzacja, pompy ciepła i usługi domowe — kompleksowo".
- Podtytuł: jedno zdanie o doborze, montażu i serwisie.
- Dwa przyciski: CTA „Darmowa konsultacja" (wariant cta) + „Zobacz ofertę" (outline).
- W tle realistyczne zdjęcie (placeholder) lekko przyciemnione dla czytelności tekstu.
- Pod hero: 3–4 mikro-atuty z ikonami (doświadczenie, gwarancja, szybki termin, raty 0%).
### 2. Dwa filary (`PillarsSection`)
- Dwa duże kafle obok siebie (na mobile pod sobą):
- „Chłód i ogrzewanie" → klimatyzacja + pompy ciepła (akcent cyjan), przycisk „Sprawdź".
- „Twój dom" → remonty, hydraulika, serwis AGD (akcent pomarańcz), przycisk „Sprawdź".
- Każdy kafel: ikona, tytuł, jedno zdanie, link.
### 3. Siatka usług (`ServicesGrid`)
- 6 kart (`IconCard`) z `content/services.ts`: Montaż klimatyzacji, Serwis i przeglądy,
Pompy ciepła, Remonty, Hydraulika, Serwis lodówek/AGD.
- Każda: ikona (lucide), tytuł, krótki opis, link do podstrony.
### 4. Proces (`ProcessSteps`)
- 4 kroki poziomo (na mobile pionowo): Kontakt i konsultacja → Wycena/audyt → Realizacja →
Uruchomienie i serwis. Numerowane, z ikonami i strzałkami.
### 5. Dlaczego my (`WhyUs` + `StatsBar`)
- 4–6 kafelków atutów + pasek liczb (`StatCounter`): lata doświadczenia, liczba realizacji,
uprawnienia F-gaz, ocena klientów. Dane z `content/company.ts`.
### 6. Polecane klimatyzatory (`FeaturedProducts`)
- 4 karty produktowe (`ProductCard`) z `content/products.ts`: zdjęcie, nazwa, cena z montażem,
przycisk „wybierz". Link „Zobacz wszystkie" → `/sklep`.
### 7. Opinie (`Testimonials`)
- 3 cytaty: imię, ocena gwiazdkowa, treść. Opcjonalnie logo Google.
### 8. Blog (`BlogPreview`)
- 3 najnowsze wpisy z `content/posts.ts`: miniatura, kategoria, tytuł, „Czytaj więcej".
### 9. CTA końcowe (`CtaBanner`)
- „Masz pytania? Umów darmową konsultację" + telefon + przycisk.
---
## SEO
- Metadata: patrz tabela w `docs/specs/03-seo.md` (wiersz „Główna").
- JSON-LD: LocalBusiness.
- Jeden H1 (hero), sekcje jako H2.
## Uwagi
- Naprzemienne tła sekcji (`bg` / `bg-muted`) dla rytmu.
- Wszystkie zdjęcia jako placeholdery z komentarzem TODO.
+60
View File
@@ -0,0 +1,60 @@
# Strona 02 — Klimatyzacja (`/klimatyzacja`)
Najbardziej rozbudowana strona usługowa. Łączy usługę (montaż/serwis) z wejściem do sklepu.
Wzorzec: rozbudowana strona montażu z konkurencji (proces + cennik + FAQ).
---
## Sekcje (kolejność)
### 1. Hero (`HeroSection`)
- Eyebrow nad H1: „15+ lat doświadczenia · Split i multisplit · Uprawnienia F-gaz".
- H1: „Montaż klimatyzacji — dobór, instalacja, serwis".
- Podtytuł + dwa CTA: „Dobierz klimatyzator" (cta) i „Sprawdź cennik" (outline, scroll do cennika).
- Lista 4 atutów z haczykami: bezpłatny audyt, estetyczny montaż, zgodność F-gaz, serwis po montażu.
### 2. Co obejmuje montaż (`InfoHighlights`)
- Krótki opis + 3 wyróżnione fakty: czas montażu (4–8 h split), cena od…, zasięg działania.
### 3. Rodzaje klimatyzatorów (`AcTypesSection`)
- 3 grupy kart: **SPLIT** (ścienne, konsolowe, kasetonowe, kanałowe), **MULTISPLIT** (2–5 pomieszczeń),
**POZOSTAŁE** (z grzaniem, z oczyszczaczem). Każda grupa: ikona, lista podtypów, link do kategorii sklepu.
### 4. Proces montażu krok po kroku (`ProcessSteps` — wariant rozszerzony)
- 8–9 ponumerowanych kroków z `content/process.ts`: przygotowanie miejsca → przewiert →
instalacja freonowa → montaż jednostek → próba szczelności → uruchomienie → szkolenie.
### 5. Konfigurator doboru (`ConfiguratorTeaser`)
- Blok z 4 krokami: marka → moc → funkcje → cena z montażem. Na tym etapie statyczny UI
(interaktywność opcjonalnie później). Obok porada „10 m² = 1 kW". Przycisk „Dobierz klimatyzator".
### 6. Cennik (`PricingTable`)
- Tabela z `content/pricing.ts`: kolumny — usługa / cena netto / brutto os. prywatna / brutto firma.
- Sekcje tabeli: split, multisplit, usługi dodatkowe.
- Pod tabelą: „Co obejmuje montaż podstawowy" (lista) + przycisk „Pobierz cennik PDF" (placeholder).
- Adnotacja o stawkach VAT 8%/23%.
### 7. Producenci (`BrandsGrid`)
- Siatka logotypów marek (placeholdery), które firma montuje.
### 8. Polecane modele (`FeaturedProducts`)
- 6–8 kart produktowych z `content/products.ts`.
### 9. Dlaczego my (`WhyUs`)
- 6 kafelków: profesjonalizm, uprawnienia F-gaz, opinie, szybkie terminy, duży wybór, lokalnie.
### 10. FAQ (`FaqAccordion`)
- Z `content/faq.ts` (zestaw „klimatyzacja"): jak dobrać moc, ile trwa montaż, zgoda wspólnoty,
VAT 8/23%, jak często przegląd, czy grzeje, czy mogę montować sam.
### 11. CTA końcowe (`CtaBanner`)
- „Niestandardowa instalacja? Zamów indywidualną wycenę" + telefon.
---
## SEO
- Metadata: wiersz „Klimatyzacja". JSON-LD: Service + FAQPage + BreadcrumbList.
## Uwagi
- Tabela cennika responsywna (na mobile: scroll poziomy lub układ kart).
- Accordion FAQ jako Client Component.
+42
View File
@@ -0,0 +1,42 @@
# Strona 03 — Pompy ciepła (`/pompy-ciepla`)
---
## Sekcje (kolejność)
### 1. Hero (`HeroSection`)
- H1: „Pompy ciepła — niższe rachunki, niezależność, dofinansowanie".
- Podtytuł + 2 CTA: „Darmowa konsultacja" (cta) + „Sprawdź dofinansowanie".
- W tle zdjęcie pompy ciepła przy domu jednorodzinnym (placeholder).
### 2. Korzyści (`BenefitsGrid`)
- 4 karty z ikonami: oszczędność, ekologia, komfort całoroczny, dotacje (Czyste Powietrze / Mój Prąd).
### 3. Rodzaje pomp (`HeatPumpTypes`)
- 3 karty: powietrze-woda, powietrze-powietrze, gruntowe. Każda: ikona, opis, dla kogo.
### 4. Dla kogo / kiedy się opłaca (`UseCases`)
- Sekcja z punktami: dom nowy, modernizacja, wymiana starego kotła. Krótkie opisy.
### 5. Proces wdrożenia (`ProcessSteps`)
- Kroki: audyt → dobór → wycena → montaż → uruchomienie → serwis.
### 6. Dofinansowanie (`FundingSection`)
- Wyróżniony blok (tło accent-light): hasło o programach dotacji, krótki opis,
przycisk „Sprawdź, ile możesz dostać". Oznacz treść jako placeholder do aktualizacji.
### 7. Realizacje pomp ciepła (`ProjectsGallery`)
- Galeria realizacji filtrowana do kategorii „pompy-ciepla" z `content/projects.ts`.
### 8. FAQ (`FaqAccordion`)
- Zestaw „pompy ciepła": koszty, oszczędności, czy działa zimą, hałas, serwis.
### 9. CTA końcowe (`CtaBanner`)
---
## SEO
- Metadata: wiersz „Pompy ciepła". JSON-LD: Service + FAQPage.
## Uwagi
- Treści o dofinansowaniach mogą się zmieniać — wyraźnie oznacz jako do aktualizacji.
+38
View File
@@ -0,0 +1,38 @@
# Strona 04 — Usługi domowe (`/uslugi-domowe`)
Strona-rozdzielnik. Zbiera wszystkie usługi „w domu" i kieruje do podstron. Prosta, czytelna.
---
## Sekcje (kolejność)
### 1. Hero (`HeroSection`)
- H1: „Usługi domowe — remonty, hydraulika i naprawy w jednym miejscu".
- Podtytuł + CTA „Darmowa konsultacja".
### 2. Siatka usług (`ServicesGrid` — wariant duży)
- 4 duże kafle (`IconCard`):
- Remonty i wykończenia → `/uslugi-domowe/remonty`
- Hydraulika → `/uslugi-domowe/hydraulika`
- Serwis AGD / lodówki → `/uslugi-domowe/serwis-agd`
- Drobne naprawy → (sekcja informacyjna lub kotwica)
- Każdy: ikona, tytuł, opis, przycisk „Sprawdź".
### 3. Jak pracujemy (`ProcessSteps`)
- 3–4 kroki: zgłoszenie → wycena → realizacja → odbiór.
### 4. Dlaczego my (`WhyUs`)
- Atuty: doświadczeni fachowcy, terminowość, sprzątanie po pracy, gwarancja.
### 5. Realizacje (`ProjectsGallery`)
- Wybrane realizacje z kategorii remonty/hydraulika, najlepiej z before/after.
### 6. CTA końcowe (`CtaBanner`)
---
## SEO
- Metadata: wiersz „Usługi domowe". JSON-LD: Service + BreadcrumbList.
## Uwagi
- Ta strona ma głównie nawigować dalej — nie przeładowuj treścią.
+65
View File
@@ -0,0 +1,65 @@
# Strony 05–07 — Podstrony usług domowych
Trzy podstrony pod `/uslugi-domowe/`. Wspólny wzorzec: hero → zakres → proces → dowody → FAQ → CTA.
---
## 05 — Remonty i wykończenia (`/uslugi-domowe/remonty`)
### Sekcje
1. **Hero** — H1 „Remonty i wykończenia wnętrz", CTA „Bezpłatna wycena".
2. **Zakres prac** (`ServicesGrid`) — karty: remonty mieszkań, łazienki, kuchnie, malowanie,
gładzie, glazura/terakota, podłogi, zabudowy GK. Ikona + krótki opis.
3. **Proces** (`ProcessSteps`) — oględziny → kosztorys → harmonogram → prace → odbiór.
4. **Galeria przed/po** (`BeforeAfterSlider`) — mocna sekcja wizualna, slider dwóch zdjęć.
Dane z `content/projects.ts` (kategoria „remonty", pole beforeAfter). Client Component.
5. **Dlaczego my** (`WhyUs`) — terminowość, czystość, własna ekipa, gwarancja.
6. **Opinie** (`Testimonials`).
7. **FAQ** (`FaqAccordion`) — zestaw „remonty": wycena, czas realizacji, materiały, gwarancja.
8. **CTA końcowe** (`CtaBanner`).
### SEO
Metadata: wiersz „Remonty". JSON-LD: Service + FAQPage + BreadcrumbList.
---
## 06 — Hydraulika (`/uslugi-domowe/hydraulika`)
### Sekcje
1. **Hero** — H1 „Usługi hydrauliczne — instalacje, naprawy, awarie", CTA + telefon awaryjny.
2. **Zakres usług** (`ServicesGrid`) — instalacje wod-kan, wymiana rur, montaż armatury,
biały montaż, ogrzewanie, awarie/przecieki, udrażnianie.
3. **Szybka pomoc / awarie** (`EmergencyBlock`) — wyróżniony blok (tło pomarańcz/akcent)
z dużym numerem telefonu i informacją o czasie reakcji.
4. **Proces** (`ProcessSteps`) — zgłoszenie → diagnoza → wycena → naprawa.
5. **Cennik orientacyjny** (`PricingTable` — uproszczona) — opcjonalnie.
6. **Dlaczego my** (`WhyUs`).
7. **FAQ** (`FaqAccordion`) — zestaw „hydraulika".
8. **CTA końcowe** (`CtaBanner`).
### SEO
Metadata: wiersz „Hydraulika". JSON-LD: Service + FAQPage + BreadcrumbList.
---
## 07 — Serwis AGD / lodówki (`/uslugi-domowe/serwis-agd`)
### Sekcje
1. **Hero** — H1 „Serwis lodówek i urządzeń chłodniczych", CTA „Zgłoś naprawę".
2. **Co naprawiamy** (`ServicesGrid`) — lodówki, zamrażarki, chłodziarki, lady i witryny
chłodnicze, agregaty.
3. **Typowe usterki** (`SymptomsList`) — lista z ikonami: nie chłodzi, głośna praca,
zbiera wodę, oszrania, nie włącza się.
4. **Proces zgłoszenia** (`ProcessSteps`) — zgłoszenie → diagnoza → wycena → naprawa → gwarancja.
5. **Dlaczego my** (`WhyUs`) — dojazd, oryginalne części, gwarancja na naprawę.
6. **FAQ** (`FaqAccordion`) — zestaw „serwis AGD".
7. **CTA końcowe** (`CtaBanner`).
### SEO
Metadata: wiersz „Serwis AGD". JSON-LD: Service + FAQPage + BreadcrumbList.
---
## Uwaga wspólna
Wszystkie trzy korzystają z tych samych komponentów sekcji co reszta strony —
różnią się treścią (z `content/`), nie strukturą. Nie twórz osobnych wariantów komponentów bez potrzeby.
@@ -0,0 +1,78 @@
# Strony 08–11 — Realizacje, Blog, O nas, Kontakt
---
## 08 — Realizacje (`/realizacje`)
### Sekcje
1. **Hero** — H1 „Nasze realizacje" + jedno zdanie.
2. **Filtry kategorii** (`ProjectFilters`) — przyciski: Wszystkie · Klimatyzacja · Pompy ciepła ·
Remonty · Hydraulika. Client Component (filtrowanie po stanie).
3. **Siatka realizacji** (`ProjectsGrid`) — karty z `content/projects.ts`: zdjęcie, tytuł,
lokalizacja, tag kategorii. Hover: powiększenie + nakładka z tytułem.
4. **Pasek liczb** (`StatsBar`) — liczba realizacji, lat, miast.
5. **CTA końcowe** (`CtaBanner`) — „Chcesz podobny efekt? Umów konsultację".
### SEO
Metadata: wiersz „Realizacje". JSON-LD: opcjonalnie ItemList.
---
## 09 — Blog (`/blog` + `/blog/[slug]`)
### Lista (`/blog`)
1. **Hero** — H1 „Blog i poradniki" + zdanie.
2. **Filtry kategorii** — Klimatyzacja, Pompy ciepła, Remonty, Hydraulika, Porady.
3. **Wyróżniony artykuł** (`FeaturedPost`) — duży kafel (post z `featured: true`).
4. **Siatka artykułów** (`PostsGrid`) — karty: miniatura, kategoria, tytuł, zajawka, data, „Czytaj więcej".
5. **Paginacja** — prosta (lub „pokaż więcej").
### Wpis (`/blog/[slug]`)
- `generateStaticParams` + `generateMetadata` z danych posta.
- Układ: tytuł (H1), meta (data, autor, kategoria), obraz główny, treść (`prose`),
sekcja „Powiązane artykuły" (3 karty), CTA do usługi powiązanej z tematem.
- JSON-LD: BlogPosting + BreadcrumbList.
### SEO
Metadata: wiersz „Blog" (lista) / dynamiczne (wpis).
---
## 10 — O nas (`/o-nas`)
### Sekcje
1. **Hero** — H1 „O nas" + zdanie misji.
2. **Historia firmy** (`AboutStory`) — tekst + zdjęcie (placeholder).
3. **Pasek statystyk** (`StatsBar`) — lata, realizacje, klienci, miasta (z `content/company.ts`).
4. **Wartości / jak pracujemy** (`ValuesGrid`) — 3–4 kafelki.
5. **Certyfikaty i uprawnienia** (`CertificatesSection`) — logotypy, F-gaz, autoryzacje (placeholdery).
6. **Opinie** (`Testimonials`).
7. **CTA końcowe** (`CtaBanner`).
### SEO
Metadata: wiersz „O nas". JSON-LD: LocalBusiness (rozszerzony).
---
## 11 — Kontakt (`/kontakt`)
### Sekcje
1. **Nagłówek** — H1 „Kontakt" + zachęta.
2. **Dwie kolumny** (`ContactSection`):
- Lewa: dane kontaktowe (telefon, e-mail, adres, godziny, NIP) — duże, klikalne.
- Prawa: **formularz** (`ContactForm`, Client Component) — RHF + Zod:
imię, telefon, e-mail, temat (select: Klimatyzacja / Pompy ciepła / Remonty / Hydraulika /
Serwis AGD / Inne), wiadomość, zgoda RODO (checkbox), przycisk „Wyślij".
Walidacja inline, stany: błąd / wysyłanie / sukces. Obsługa przez Server Action
w `app/kontakt/actions.ts` (na razie bez realnej wysyłki — miejsce na integrację, np. Resend).
3. **Mapa** (`MapEmbed`) — Google Maps na całą szerokość (placeholder/iframe).
4. **Szybki kontakt** — przyciski „Zadzwoń" / „Napisz".
### SEO
Metadata: wiersz „Kontakt". JSON-LD: LocalBusiness.
---
## Uwaga
Formularz: poprawna dostępność (labelki powiązane z polami, komunikaty błędów czytane przez czytniki),
focus na pierwszym błędnym polu, blokada podwójnego wysłania.
+80
View File
@@ -0,0 +1,80 @@
# Strony 12–15 — E-commerce
Sklep z klimatyzatorami. Stan koszyka przez Zustand (`lib/cart-store.ts`, persist do localStorage).
---
## 12 — Sklep / listing (`/sklep`)
### Sekcje
1. **Nagłówek kategorii** — H1 + krótki opis SEO.
2. **Pasek narzędzi** — liczba wyników + sortowanie (cena rosnąco/malejąco, popularność, nowości).
3. **Układ dwukolumnowy:**
- Lewa (`ProductFilters`, Client) — filtry: producent (checkboxy), moc (zakres/checkboxy),
cena (suwak), typ/kategoria, funkcje (Wi-Fi, grzanie). Na mobile: panel wysuwany.
- Prawa (`ProductsGrid`) — siatka kart (`ProductCard`): zdjęcie, marka, nazwa, moc,
cena, cena z montażem, przycisk „wybierz", ikona serca (lista życzeń — UI).
4. **Paginacja** lub „pokaż więcej".
5. **Blok opisowy SEO** na dole.
### Dane
Z `content/products.ts` przez `getProducts()`. Filtrowanie/sortowanie po stronie klienta.
---
## 13 — Karta produktu (`/sklep/[slug]`)
- `generateStaticParams` + `generateMetadata` z danych produktu.
### Układ
1. **Górna część — dwie kolumny:**
- Lewa (`ProductGallery`, Client) — duże zdjęcie + miniatury.
- Prawa (`ProductInfo`) — marka, nazwa (H1), cena, cena z montażem (VAT 8%),
wybór wariantu mocy (jeśli są warianty), przyciski „Dodaj do koszyka" i „Kup z montażem",
ikony: lista życzeń, porównanie. Atuty: dostawa, gwarancja, raty.
2. **Zakładki** (`Tabs`, Client) — Opis / Dane techniczne (tabela ze `specs`) / Opinie.
3. **Podobne produkty** (`FeaturedProducts`) — 4 karty z tej samej kategorii.
### Akcje
„Dodaj do koszyka" → `cart-store` (dodaje produkt + wybrany wariant). Feedback (toast/licznik w headerze).
### JSON-LD
Product + Offer + BreadcrumbList.
---
## 14 — Koszyk (`/koszyk`)
### Układ
1. **Lista pozycji** (`CartItems`, Client) — miniatura, nazwa, wariant, ilość (− / +),
cena jednostkowa, suma pozycji, usuń. Opcja „dodaj montaż" przy pozycji.
2. **Podsumowanie** (`CartSummary`) — wartość, VAT, suma, pole kodu rabatowego (UI),
przycisk „Przejdź do zamówienia".
3. **Pusty koszyk** — stan pusty z komunikatem + przycisk „Przejdź do sklepu".
### Dane
Stan z `cart-store` (Zustand, persist). Bez realnego checkoutu na tym etapie —
przycisk prowadzi do placeholdera / strony „w przygotowaniu" lub formularza zamówienia (do ustalenia).
---
## 15 — Raty 0% (`/raty`)
### Sekcje
1. **Hero** — H1 + hasło o ratach 0%.
2. **Jak to działa** (`ProcessSteps`) — kroki wnioskowania.
3. **Kalkulator rat** (`InstallmentCalculator`, Client) — suwak kwoty → wyświetla ratę miesięczną
(prosty wzór: kwota / liczba rat, np. do 20 rat 0%). Bez integracji finansowej — kalkulacja poglądowa.
4. **Warunki** — lista warunków (placeholder).
5. **FAQ** (`FaqAccordion`).
6. **CTA** — „Złóż wniosek" (placeholder / link do kontaktu).
### Uwaga
Oznacz wszystkie liczby i warunki jako poglądowe/do uzupełnienia — to treści zależne od oferty banku.
---
## Uwaga wspólna (e-commerce)
- Komponenty sklepu w `components/shop/`.
- Ceny i produkty to mock — wyraźnie oznaczone, łatwe do podmiany.
- Dostępność: przyciski z aria-label, liczniki ilości obsługiwane z klawiatury.
+32
View File
@@ -0,0 +1,32 @@
# Strona 16 — Strony formalne
Pięć prostych stron tekstowych w route group `(formalne)`. Wspólny layout jednokolumnowy.
---
## Strony
- `/regulamin`
- `/polityka-prywatnosci`
- `/reklamacje`
- `/dostawa`
- `/gwarancja`
## Layout (`app/(formalne)/layout.tsx`)
- Ten sam Header/Footer co reszta.
- Treść w jednej kolumnie, styl `prose`, czytelne nagłówki sekcji.
- Na dłuższych stronach (regulamin, polityka prywatności): spis treści z kotwicami po lewej
(sticky na desktopie).
## Treść
- Placeholdery z realistyczną strukturą sekcji (np. polityka prywatności: administrator danych,
zakres, cele, cookies, prawa użytkownika, kontakt).
- Wyraźnie oznacz, że treść prawna wymaga uzupełnienia/weryfikacji przez właściciela
(`{/* TODO: treść prawna do uzupełnienia */}`).
- Nie generuj wiążących treści prawnych jako gotowych — to ma być rusztowanie do wypełnienia.
## SEO
- Metadata podstawowe (title + description). `robots`: index, follow (lub noindex dla wybranych — do decyzji).
- JSON-LD: niepotrzebne.
## Uwaga
Te strony są linkowane ze stopki. Upewnij się, że linki w footerze prowadzą do istniejących tras.
+99
View File
@@ -0,0 +1,99 @@
# 01 — Architektura
Definiuje strukturę projektu, routing i wzorce. Trzymaj się jej ściśle.
---
## Struktura katalogów
```
.
├── AGENTS.md
├── app/
│ ├── layout.tsx # root layout: font, header, footer, metadata bazowe
│ ├── page.tsx # strona główna
│ ├── globals.css # Tailwind + CSS variables (tokeny)
│ ├── klimatyzacja/
│ │ └── page.tsx
│ ├── pompy-ciepla/
│ │ └── page.tsx
│ ├── uslugi-domowe/
│ │ ├── page.tsx # rozdzielnik
│ │ ├── remonty/page.tsx
│ │ ├── hydraulika/page.tsx
│ │ └── serwis-agd/page.tsx
│ ├── realizacje/
│ │ └── page.tsx
│ ├── blog/
│ │ ├── page.tsx
│ │ └── [slug]/page.tsx
│ ├── o-nas/page.tsx
│ ├── kontakt/
│ │ ├── page.tsx
│ │ └── actions.ts # Server Action formularza
│ ├── sklep/
│ │ ├── page.tsx # listing + filtry
│ │ └── [slug]/page.tsx # karta produktu
│ ├── koszyk/page.tsx
│ ├── raty/page.tsx
│ ├── (formalne)/ # route group dla stron formalnych
│ │ ├── regulamin/page.tsx
│ │ ├── polityka-prywatnosci/page.tsx
│ │ ├── reklamacje/page.tsx
│ │ ├── dostawa/page.tsx
│ │ └── gwarancja/page.tsx
│ ├── sitemap.ts
│ ├── robots.ts
│ ├── not-found.tsx
│ └── loading.tsx
├── components/
│ ├── ui/ # shadcn/ui (button, card, accordion, input, ...)
│ ├── layout/ # Header, Footer, MobileMenu, TrustBar, NavDropdown
│ ├── sections/ # sekcje stron: Hero, ServicesGrid, ProcessSteps, FAQ, ...
│ ├── shop/ # ProductCard, ProductFilters, CartSummary, ...
│ └── common/ # SectionHeading, CTAButton, StatCounter, Placeholder, ...
├── content/ # dane (mock): products.ts, projects.ts, posts.ts, faq.ts, ...
├── lib/ # utils.ts, constants.ts, cart-store.ts (zustand), validations.ts (zod)
├── types/ # product.ts, project.ts, post.ts, ...
├── public/ # zdjęcia placeholder, logo, ikony, og-images
├── tailwind.config.ts
└── tsconfig.json # alias @/*
```
---
## Zasady routingu
- App Router, Server Components domyślnie.
- Strony formalne w route group `(formalne)` — wspólny, prosty layout tekstowy.
- Dynamiczne trasy: `blog/[slug]`, `sklep/[slug]` — `generateStaticParams` z danych w `content/`.
- Każda strona eksportuje `metadata` (lub `generateMetadata` dla dynamicznych).
---
## Wzorce komponentów
1. **Strona = kompozycja sekcji.** `page.tsx` importuje komponenty z `components/sections/`
i przekazuje im dane. Strona nie zawiera logiki layoutu sekcji.
2. **Sekcja = jeden komponent.** Np. `HeroSection`, `ServicesGrid`, `ProcessSteps`, `FaqAccordion`,
`PricingTable`, `Testimonials`, `StatsBar`, `BlogPreview`, `CtaBanner`.
3. **Współdzielone bloki** (Hero, FAQ, CTA) przyjmują propsy, by można było ich użyć na wielu stronach
z różną treścią — nie duplikuj.
4. **Dane oddzielone od prezentacji** — komponent dostaje dane przez propsy, źródło to `content/`.
5. **Client Components** tylko: formularze, koszyk (Zustand), dropdowny nawigacji, mobile menu,
slidery galerii, kalkulator rat, filtry sklepu.
---
## Stan aplikacji
- Koszyk: Zustand store w `lib/cart-store.ts` (dodaj/usuń/zmień ilość/suma). Persist do localStorage.
- Reszta: stan lokalny komponentów. Bez globalnego store poza koszykiem.
---
## Obsługa obrazów
- Wszystkie zdjęcia jako placeholdery na tym etapie (komponent `Placeholder` lub pliki w `public/`).
- W kodzie oznacz komentarzem `{/* TODO: docelowe zdjęcie — opis */}`, gdzie wstawić realne foto.
- Zawsze `next/image` z `alt`, `width/height` lub `fill` + `sizes`.
+88
View File
@@ -0,0 +1,88 @@
# 02 — Design System
Pełna definicja systemu projektowego. Zaimplementuj jako tokeny w `tailwind.config.ts`
i CSS variables w `globals.css`. Wszystkie komponenty korzystają z tokenów, nie z wartości „na sztywno".
---
## Paleta kolorów
| Token | Hex | Zastosowanie |
|-------|-----|--------------|
| `primary` (granat) | `#0F2A47` | nagłówki, header, footer, tekst akcentowany |
| `primary-light` | `#1B3D63` | hover na elementach primary |
| `accent` (cyjan) | `#2BA8E0` | akcenty „chłodu", linki, ikony, podkreślenia |
| `accent-light` | `#E3F4FC` | tła sekcji związanych z klimatyzacją |
| `cta` (pomarańcz) | `#F39320` | przyciski akcji, wyróżnienia konwersji |
| `cta-hover` | `#D97D0D` | hover przycisków CTA |
| `bg` | `#FFFFFF` | główne tło |
| `bg-muted` | `#F5F7FA` | tła naprzemiennych sekcji |
| `text` | `#1A2230` | tekst podstawowy |
| `text-muted` | `#5A6678` | tekst drugorzędny, opisy |
| `border` | `#E2E8F0` | obramowania kart, separatory |
| `success` | `#16A34A` | potwierdzenia, „dostępne" |
Tryb: na start jasny. Przygotuj zmienne tak, by dark mode dało się dodać później bez przepisywania.
---
## Typografia
- Font: **Inter** lub **Poppins** przez `next/font/google` (subset latin + latin-ext dla polskich znaków).
- Skala (desktop / mobile):
- H1: 48–56px / 32–36px, bold, line-height 1.1
- H2: 36–40px / 28px, bold
- H3: 24–28px / 20px, semibold
- Body: 16–18px / 16px, regular, line-height 1.6
- Small: 14px
- Nagłówki w kolorze `primary`. Tekst w `text`. Opisy w `text-muted`.
- Maksymalna szerokość bloku tekstu: ~70 znaków (czytelność).
---
## Spacing i layout
- Kontener: max-width 1280px, padding boczny 16px (mobile) / 24–32px (desktop), wyśrodkowany.
- Odstępy między sekcjami: 80–120px desktop / 48–64px mobile. Dużo powietrza.
- Siatka kart: grid responsywny (1 kol. mobile → 2 → 3/4 desktop), gap 24px.
- Sekcje naprzemiennie `bg` i `bg-muted` dla rytmu wizualnego.
---
## Komponenty UI (na bazie shadcn/ui, dostosowane do tokenów)
- **Button** — warianty: `cta` (pomarańcz, główny), `primary` (granat), `outline`, `ghost`.
Zaokrąglenie `rounded-lg`, padding wygodny, wyraźny hover i focus ring.
- **Card** — `rounded-xl`, `border`, subtelny cień (`shadow-sm` → `shadow-md` na hover dla klikalnych).
- **Accordion** — do FAQ. Płynne rozwijanie, ikona +/− lub chevron.
- **Input / Textarea / Select** — spójne obramowania, focus w kolorze `accent`, czytelne labelki.
- **Badge / Tag** — do kategorii (realizacje, blog) i wyróżnień produktów.
- **Tabs** — do zakładek na karcie produktu.
---
## Wspólne komponenty własne (`components/common/`)
- **SectionHeading** — eyebrow (mały tekst nad), H2, opcjonalny podtytuł. Wyśrodkowany lub do lewej.
- **CTAButton** — przycisk z ikoną, wariant cta, używany w hero i banerach.
- **StatCounter** — duża liczba + podpis (np. „3500 realizacji").
- **IconCard** — ikona + tytuł + opis + opcjonalny link. Baza dla siatek usług.
- **StepItem** — numer + tytuł + opis. Baza dla procesów krok-po-kroku.
- **Placeholder** — szary blok z ikoną i opisem zamiast docelowego zdjęcia.
---
## Stany i interakcje
- Hover: subtelne podniesienie cienia / zmiana koloru, transition 150–200ms.
- Focus: widoczny ring w kolorze `accent` (dostępność).
- Loading: skeleton dla list (sklep, blog).
- Puste stany: czytelny komunikat + CTA (np. pusty koszyk).
---
## Zasada nadrzędna
Design ma wyglądać jak zaprojektowany przez człowieka: spójny, czysty, z oddechem.
Unikaj efektu „szablonu AI": przeładowania gradientami, zbyt wielu kolorów, ciasnych sekcji,
losowych cieni. Mniej, ale konsekwentnie.

Some files were not shown because too many files have changed in this diff Show More