feat: implement compare functionality, checkout flow, and add 30 blog posts

This commit is contained in:
MichalC
2026-06-27 18:09:22 +02:00
parent 6bdfe18e18
commit 0c59086a6b
63 changed files with 9766 additions and 1874 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
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.',
title: 'Dostawa i Płatności | ThermCool',
description: 'Koszty, formy i szacowane czasy dostawy urządzeń w sklepie ThermCool.',
alternates: {
canonical: '/dostawa',
}
+1 -1
View File
@@ -3,7 +3,7 @@ import Link from 'next/link';
import { ShieldCheck } from 'lucide-react';
export const metadata: Metadata = {
title: 'Gwarancja na Klimatyzatory i Montaż | AirProject',
title: 'Gwarancja na Klimatyzatory i Montaż | ThermCool',
description: 'Szczegółowe informacje dotyczące warunków gwarancji na instalowane urządzenia oraz usługi montażowe.',
alternates: {
canonical: '/gwarancja',
+2 -2
View File
@@ -2,8 +2,8 @@ import { Metadata } from 'next';
import { companyDetails } from '@/content/company';
export const metadata: Metadata = {
title: 'Polityka Prywatności i Cookies | AirProject',
description: 'Zasady przetwarzania danych osobowych oraz wykorzystania plików cookies w serwisie AirProject.',
title: 'Polityka Prywatności | ThermCool',
description: 'Zasady przetwarzania danych osobowych oraz wykorzystania plików cookies w serwisie ThermCool.',
alternates: {
canonical: '/polityka-prywatnosci',
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Regulamin Sklepu i Usług | AirProject',
title: 'Regulamin Sklepu i Usług | ThermCool',
description: 'Regulamin świadczenia usług, zasady korzystania ze sklepu, prawa i obowiązki klienta.',
alternates: {
canonical: '/regulamin',
+2 -2
View File
@@ -2,8 +2,8 @@ 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.',
title: 'Reklamacje i Zwroty | ThermCool',
description: 'Zasady i procedury składania reklamacji oraz zwrotów produktów i usług w sklepie ThermCool.',
alternates: {
canonical: '/reklamacje',
}
+1 -3
View File
@@ -33,7 +33,7 @@ export async function generateMetadata({ params }: BlogPostPageProps): Promise<M
}
return {
title: `${post.title} | Blog AirProject`,
title: `${post.title} | Blog ThermCool`,
description: post.excerpt,
alternates: {
canonical: `/blog/${post.slug}`,
@@ -81,8 +81,6 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
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';
}
};
+1 -1
View File
@@ -6,7 +6,7 @@ import { CtaBanner } from '@/components/sections/CtaBanner';
import { getPosts, getFeaturedPost } from '@/content/posts';
export const metadata: Metadata = {
title: 'Blog i Porady | AirProject',
title: 'Blog i Porady | ThermCool',
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',
+2 -2
View File
@@ -75,9 +75,9 @@
--accent: #2ba8e0;
--accent-foreground: #ffffff;
--accent-light: #e3f4fc;
--cta: #1e293b;
--cta: #F39320;
--cta-foreground: #ffffff;
--cta-hover: #0f172a;
--cta-hover: #D97D0D;
--destructive: oklch(0.577 0.245 27.325);
--theme-border: #e2e8f0;
--input: #e2e8f0;
+1 -1
View File
@@ -5,7 +5,7 @@ import { companyDetails } from "@/content/company";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Kalkulator - W przygotowaniu | AIR Project",
title: 'Kalkulator doboru | ThermCool',
description: "Kalkulator doboru klimatyzacji i pomp ciepła w przygotowaniu.",
};
+235
View File
@@ -0,0 +1,235 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { MapPin, Clock, Phone, ArrowRight, ThermometerSun } from 'lucide-react';
import { getCityBySlug, getAllCitySlugs, dolnySlaskCities } from '@/content/cities';
import { HeroSection } from '@/components/sections/HeroSection';
import { ProcessSteps } from '@/components/sections/ProcessSteps';
import { PricingTable } from '@/components/sections/PricingTable';
import { WhyUs } from '@/components/sections/WhyUs';
import { FaqAccordion } from '@/components/sections/FaqAccordion';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { Testimonials } from '@/components/sections/Testimonials';
import { acProcessSteps } from '@/content/process';
import { companyDetails } from '@/content/company';
interface Props {
params: Promise<{ miasto: string }>;
}
export async function generateStaticParams() {
return getAllCitySlugs().map((c) => ({ miasto: c.slug }));
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { miasto } = await params;
const city = getCityBySlug(miasto);
if (!city) return { title: 'Nie znaleziono' };
return {
title: city.seoTitle,
description: city.seoDescription,
alternates: { canonical: `/klimatyzacja/${city.slug}` },
openGraph: {
title: city.seoTitle,
description: city.seoDescription,
},
};
}
export default async function MiastoPage({ params }: Props) {
const { miasto } = await params;
const city = getCityBySlug(miasto);
if (!city) notFound();
const otherCities = dolnySlaskCities.filter((c) => c.slug !== city.slug);
const jsonLd = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": companyDetails.name,
"telephone": companyDetails.phone,
"email": companyDetails.email,
"url": `${process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl'}/klimatyzacja/${city.slug}`,
"address": {
"@type": "PostalAddress",
"streetAddress": companyDetails.address.street,
"addressLocality": companyDetails.address.city,
"postalCode": companyDetails.address.zipCode,
"addressCountry": "PL",
},
"areaServed": { "@type": "City", "name": city.name },
"priceRange": "$$",
};
return (
<main>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* HERO */}
<HeroSection
eyebrow={`Klimatyzacja ${city.name} · Dolny Śląsk · F-Gaz`}
title={`Montaż klimatyzacji w ${city.nameDopelniacz}`}
subtitle={city.heroSubtitle}
ctaText="Zamów bezpłatną wycenę"
ctaLink="/kontakt"
secondaryCtaText="Zobacz klimatyzatory"
secondaryCtaLink="/sklep"
backgroundImage={city.heroImage}
features={[
'Bezpłatny audyt i dobór',
'Uprawnienia F-Gaz',
'Gwarancja do 5 lat',
`Dojazd do ${city.nameDopelniacz2}`,
]}
/>
{/* INTRO + FAKTY LOKALNE */}
<section className="py-16 md:py-20 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-12 items-start">
<div>
<span className="text-xs font-semibold uppercase tracking-wider text-accent font-heading">
Klimatyzacja {city.name}
</span>
<h2 className="text-2xl md:text-3xl font-bold text-primary font-heading mt-2 mb-4">
Profesjonalny montaż klimatyzacji w {city.nameDopelniacz}
</h2>
<p className="text-text-muted leading-relaxed mb-4">{city.introText}</p>
<p className="text-text-muted leading-relaxed">{city.localFacts}</p>
</div>
<div className="flex flex-col gap-4">
{/* Highlights */}
<div className="p-5 rounded-xl bg-slate-50 border border-border flex gap-4 items-start">
<Clock className="size-5 text-accent shrink-0 mt-0.5" />
<div>
<p className="font-semibold text-primary text-sm">Czas montażu</p>
<p className="text-text-muted text-sm mt-1">Standardowy montaż split w {city.nameDopelniacz} trwa 4–8 godzin. Multisplit — 1–2 dni robocze.</p>
</div>
</div>
<div className="p-5 rounded-xl bg-slate-50 border border-border flex gap-4 items-start">
<MapPin className="size-5 text-accent shrink-0 mt-0.5" />
<div>
<p className="font-semibold text-primary text-sm">Zasięg: {city.name} i okolice</p>
<p className="text-text-muted text-sm mt-1">{city.distanceFromHQ === 'siedziba firmy' ? 'To miasto, w którym mieści się nasza siedziba.' : `Jesteśmy ${city.distanceFromHQ}. Obsługujemy miasto i okoliczne gminy w promieniu 30 km.`}</p>
</div>
</div>
<div className="p-5 rounded-xl bg-accent-light border border-accent/20 flex gap-4 items-start">
<Phone className="size-5 text-accent shrink-0 mt-0.5" />
<div>
<p className="font-semibold text-primary text-sm">Bezpłatna wycena</p>
<p className="text-text-muted text-sm mt-1">Zadzwoń lub napisz — wyceniamy bezpłatnie, bez zobowiązań. Odpowiadamy w ciągu godziny.</p>
<a href={`tel:${companyDetails.phoneRaw}`} className="inline-flex items-center gap-1.5 text-accent font-semibold text-sm mt-2 hover:underline">
{companyDetails.phone} <ArrowRight className="size-3.5" />
</a>
</div>
</div>
</div>
</div>
</div>
</section>
{/* HIGHLIGHT KLIMATYZACJI */}
<section className="py-12 bg-slate-50 border-t border-b border-border">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<p className="text-base md:text-lg text-text-muted leading-relaxed italic">
{city.acHighlight}
</p>
</div>
</section>
{/* PROCES MONTAŻU */}
<ProcessSteps
steps={acProcessSteps}
eyebrow="Jak Działamy"
title={`Montaż klimatyzacji w ${city.nameDopelniacz} — krok po kroku`}
subtitle="Od pierwszego kontaktu do przekazania gotowej instalacji. Każdy etap pod kontrolą."
/>
{/* CENNIK */}
<PricingTable />
{/* POMPY CIEPŁA CTA */}
<section className="py-16 bg-primary">
<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-10 items-center">
<div>
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-accent/20 border border-accent/30 text-white text-xs font-semibold uppercase tracking-wider mb-4 font-heading">
<ThermometerSun className="size-3.5 text-accent" />
Pompy ciepła {city.name}
</div>
<h2 className="text-2xl md:text-3xl font-bold text-white font-heading leading-tight mb-4">
Pompa ciepła w {city.nameDopelniacz}
</h2>
<p className="text-white/80 leading-relaxed mb-6">{city.heatPumpTeaser}</p>
<Link href="/pompy-ciepla">
<button className="btn btn-primary font-bold text-white flex items-center gap-2">
Dowiedz się więcej o pompach ciepła
<ArrowRight className="size-4" />
</button>
</Link>
</div>
<div className="grid grid-cols-2 gap-4">
{[
{ label: 'Dofinansowanie', value: 'do 135 000 zł', sub: 'z programu Czyste Powietrze' },
{ label: 'Oszczędność', value: 'do 60%', sub: 'niższe koszty ogrzewania' },
{ label: 'Sprawność', value: 'COP 4–5', sub: 'na każdy 1 kW energii' },
{ label: 'Gwarancja', value: '5 lat', sub: 'na urządzenie i montaż' },
].map((item, idx) => (
<div key={idx} className="p-4 rounded-xl bg-white/10 border border-white/20 text-center">
<p className="text-xs text-white/60 uppercase tracking-wider font-heading mb-1">{item.label}</p>
<p className="text-2xl font-bold text-accent font-heading">{item.value}</p>
<p className="text-xs text-white/70 mt-1">{item.sub}</p>
</div>
))}
</div>
</div>
</div>
</section>
{/* DLACZEGO MY */}
<WhyUs />
{/* OPINIE */}
<Testimonials />
{/* FAQ */}
<FaqAccordion category="klimatyzacja" />
{/* INNE MIASTA */}
<section className="py-16 bg-slate-50 border-t border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-8">
<span className="text-xs font-semibold uppercase tracking-wider text-accent font-heading">
Zasięg działania
</span>
<h2 className="text-xl md:text-2xl font-bold text-primary font-heading mt-2">
Klimatyzacja na Dolnym Śląsku — inne miasta
</h2>
</div>
<div className="flex flex-wrap justify-center gap-3">
{otherCities.map((c) => (
<Link
key={c.slug}
href={`/klimatyzacja/${c.slug}`}
className="px-4 py-2 rounded-full bg-white border border-border text-sm font-medium text-primary hover:border-accent hover:text-accent transition-colors shadow-sm"
>
Klimatyzacja {c.name}
</Link>
))}
</div>
</div>
</section>
{/* KOŃCOWE CTA */}
<CtaBanner
title={`Potrzebujesz klimatyzacji w ${city.nameDopelniacz}?`}
subtitle="Odpowiemy w ciągu godziny i umówimy bezpłatny audyt w dogodnym terminie."
ctaText="Zadzwoń lub napisz"
ctaLink="/kontakt"
/>
</main>
);
}
+31 -2
View File
@@ -11,9 +11,10 @@ import { WhyUs } from '@/components/sections/WhyUs';
import { FaqAccordion } from '@/components/sections/FaqAccordion';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { acProcessSteps } from '@/content/process';
import { dolnySlaskCities } from '@/content/cities';
import Link from 'next/link';
export const metadata: Metadata = {
title: 'Montaż i Serwis Klimatyzacji | AirProject',
title: 'Montaż i Serwis Klimatyzacji | ThermCool',
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',
@@ -82,6 +83,34 @@ export default function KlimatyzacjaPage() {
<FaqAccordion category="klimatyzacja" />
{/* Sekcja miast dla SEO */}
<section id="miasta" className="py-16 bg-slate-50 border-t border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-10">
<span className="text-xs font-semibold uppercase tracking-wider text-accent font-heading">
Zasięg działania
</span>
<h2 className="text-2xl md:text-3xl font-bold text-primary font-heading mt-2">
Montaż klimatyzacji na Dolnym Śląsku
</h2>
<p className="text-text-muted mt-3 max-w-xl mx-auto">
Działamy na terenie całego Dolnego Śląska. Wybierz swoje miasto i dowiedz się więcej:
</p>
</div>
<div className="flex flex-wrap justify-center gap-3">
{dolnySlaskCities.map((city) => (
<Link
key={city.slug}
href={`/klimatyzacja/${city.slug}`}
className="px-4 py-2 rounded-full bg-white border border-border text-sm font-medium text-primary hover:border-accent hover:text-accent transition-colors shadow-sm"
>
Klimatyzacja {city.name}
</Link>
))}
</div>
</div>
</section>
<CtaBanner
title="Niestandardowa instalacja?"
subtitle="Potrzebujesz klimatyzacji w zabytkowym budynku lub masz specyficzne wymagania architektoniczne? Skontaktuj się z nami po indywidualną wycenę."
+2 -2
View File
@@ -4,7 +4,7 @@ import { ContactSection } from '@/components/sections/ContactSection';
import { MapEmbed } from '@/components/sections/MapEmbed';
export const metadata: Metadata = {
title: 'Kontakt | AirProject',
title: 'Kontakt | ThermCool',
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',
@@ -17,7 +17,7 @@ export default function KontaktPage() {
<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ń."
subtitle="Zastanawiasz się nad montażem klimatyzacji lub pompy ciepła? Potrzebujesz serwisu? Napisz lub zadzwoń — odpowiemy szybko i bezpłatnie."
ctaText="Zadzwoń do nas"
ctaLink="tel:+48123456789" // TODO: Use real phone from config
secondaryCtaText="Napisz maila"
+86 -28
View File
@@ -1,39 +1,97 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { CartItems } from '@/components/shop/CartItems';
import { CartSummary } from '@/components/shop/CartSummary';
'use client';
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',
}
};
import { useCartStore } from '@/lib/cart-store';
import Image from 'next/image';
import Link from 'next/link';
import { Trash2, Plus, Minus, ShoppingBag, ArrowRight } from 'lucide-react';
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"
/>
const { items, removeItem, updateQuantity, getTotal, clearCart } = useCartStore();
const total = getTotal();
<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 />
if (items.length === 0) {
return (
<main className="min-h-screen bg-slate-50 flex items-center justify-center">
<div className="text-center">
<ShoppingBag className="size-16 text-border mx-auto mb-4" />
<h1 className="text-2xl font-bold text-primary font-heading mb-2">Koszyk jest pusty</h1>
<p className="text-text-muted mb-6">Dodaj produkty ze sklepu aby złożyć zamówienie.</p>
<Link href="/sklep">
<button className="btn btn-primary text-white font-bold">Przejdź do sklepu</button>
</Link>
</div>
</main>
);
}
return (
<main className="bg-slate-50 min-h-screen py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-bold text-primary font-heading mb-8">Twój koszyk</h1>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Lista produktów */}
<div className="lg:col-span-2 flex flex-col gap-4">
{items.map((item) => (
<div key={item.id} className="bg-white rounded-xl border border-border p-4 flex gap-4 items-center">
<div className="relative size-20 bg-slate-50 rounded-lg shrink-0">
<Image src={item.image} alt={item.name} fill className="object-contain p-2" unoptimized={item.image.startsWith('http')} />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-accent font-semibold">{item.brand}</p>
<p className="text-sm font-semibold text-primary font-heading leading-snug line-clamp-2">{item.name}</p>
<p className="text-base font-bold text-primary mt-1">{item.price.toLocaleString('pl-PL')} zł</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button onClick={() => updateQuantity(item.id, item.quantity - 1)} className="size-7 rounded-lg border border-border flex items-center justify-center text-primary hover:border-accent hover:text-accent transition-colors">
<Minus className="size-3.5" />
</button>
<span className="text-sm font-bold text-primary w-6 text-center">{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)} className="size-7 rounded-lg border border-border flex items-center justify-center text-primary hover:border-accent hover:text-accent transition-colors">
<Plus className="size-3.5" />
</button>
</div>
<p className="text-base font-bold text-primary shrink-0 w-24 text-right">
{(item.price * item.quantity).toLocaleString('pl-PL')} zł
</p>
<button onClick={() => removeItem(item.id)} className="text-text-muted hover:text-red-500 transition-colors shrink-0">
<Trash2 className="size-4" />
</button>
</div>
))}
</div>
{/* Podsumowanie */}
<div className="lg:col-span-1">
<CartSummary />
<div className="bg-white rounded-xl border border-border p-6 sticky top-24">
<h2 className="text-base font-bold text-primary font-heading mb-4">Podsumowanie</h2>
<div className="flex justify-between text-sm text-text-muted mb-2">
<span>Produkty ({items.reduce((s, i) => s + i.quantity, 0)} szt.)</span>
<span>{total.toLocaleString('pl-PL')} zł</span>
</div>
<div className="flex justify-between text-sm text-text-muted mb-4">
<span>Montaż</span>
<span className="text-accent font-medium">wycena telefoniczna</span>
</div>
<div className="border-t border-border pt-4 mb-6">
<div className="flex justify-between font-bold text-primary">
<span>Razem (urządzenia)</span>
<span>{total.toLocaleString('pl-PL')} zł</span>
</div>
<p className="text-xs text-text-muted mt-1">+ koszt montażu ustalany indywidualnie</p>
</div>
<Link href="/zamowienie" className="block">
<button className="btn btn-primary text-white font-bold w-full flex items-center justify-center gap-2">
Złóż zamówienie <ArrowRight className="size-4" />
</button>
</Link>
<button onClick={clearCart} className="w-full text-xs text-text-muted hover:text-primary transition-colors mt-3">
Wyczyść koszyk
</button>
</div>
</div>
</div>
</section>
</div>
</main>
);
}
+5 -3
View File
@@ -5,6 +5,7 @@ import { TrustBar } from "@/components/layout/TrustBar";
import { Footer } from "@/components/layout/Footer";
import { CookieBanner } from "@/components/ui/CookieBanner";
import { AnalyticsWrapper } from "@/components/ui/AnalyticsWrapper";
import { CompareBar } from '@/components/shop/CompareBar';
import "./globals.css";
const fontSans = Inter({
@@ -20,7 +21,7 @@ const fontHeading = Poppins({
export const metadata: Metadata = {
title: "ThermCool | Klimatyzacja i Pompy Ciepła",
description: "Zapewniamy profesjonalny montaż klimatyzacji, pomp ciepła oraz kompleksowe usługi remontowo-wykończeniowe. Sprawdź naszą ofertę!",
description: "Profesjonalny montaż i serwis klimatyzacji oraz pomp ciepła na Dolnym Śląsku. Uprawnienia F-Gaz, bezpłatny audyt, estetyczny montaż i pełna gwarancja. Zadzwoń po wycenę!",
};
export default function RootLayout({
@@ -31,15 +32,16 @@ export default function RootLayout({
return (
<html
lang="pl"
className={`${fontSans.variable} ${fontHeading.variable} h-full antialiased`}
className={`${fontSans.variable} ${fontHeading.variable} antialiased`}
>
<body className="min-h-full flex flex-col bg-background text-foreground">
<body className="min-h-screen flex flex-col bg-background text-foreground">
<Header />
<main className="flex-grow">{children}</main>
<TrustBar />
<Footer />
<CookieBanner />
<AnalyticsWrapper />
<CompareBar />
</body>
</html>
);
+7
View File
@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-muted">
<div className="w-16 h-16 bg-primary rounded-full animate-pulse shadow-md"></div>
</div>
);
}
+17
View File
@@ -0,0 +1,17 @@
import Link from 'next/link';
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-[70vh] bg-muted px-4 text-center">
<h1 className="text-4xl md:text-5xl font-bold font-heading text-primary mb-4">
Strona nie istnieje
</h1>
<p className="text-base md:text-lg text-slate-500 mb-8 max-w-md mx-auto">
Szukana strona została usunięta lub nigdy nie istniała.
</p>
<Link href="/" className="btn btn-primary text-white font-bold">
Wróć na stronę główną
</Link>
</div>
);
}
+2 -2
View File
@@ -9,8 +9,8 @@ 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.',
title: 'O Nas | ThermCool',
description: 'Poznaj zespół specjalistów z ThermCool. Zobacz naszą historię, wartości oraz certyfikaty poświadczające najwyższą jakość usług instalacyjnych i remontowych.',
alternates: {
canonical: '/o-nas',
}
+1 -1
View File
@@ -13,7 +13,7 @@ export default function Home() {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": companyDetails.name,
"image": "http://localhost:3000/logo.png",
"image": `${process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl'}/logo.png`,
"telephone": companyDetails.phone,
"email": companyDetails.email,
"address": {
+1 -1
View File
@@ -11,7 +11,7 @@ import { CtaBanner } from '@/components/sections/CtaBanner';
import { heatPumpProcessSteps } from '@/content/process';
export const metadata: Metadata = {
title: 'Pompy Ciepła — Montaż i Dobór | AirProject',
title: 'Pompy Ciepła — Montaż i Dobór | ThermCool',
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',
+104
View File
@@ -0,0 +1,104 @@
'use client';
import { useCompareStore } from '@/lib/compare-store';
import Image from 'next/image';
import Link from 'next/link';
import { X, ShoppingCart } from 'lucide-react';
import { useCartStore } from '@/lib/cart-store';
export default function PorownajPage() {
const { items, removeItem } = useCompareStore();
const addToCart = useCartStore((s) => s.addItem);
if (items.length === 0) {
return (
<main className="min-h-screen bg-slate-50 flex items-center justify-center">
<div className="text-center">
<p className="text-text-muted mb-4">Nie masz żadnych produktów do porównania.</p>
<Link href="/sklep">
<button className="btn btn-primary text-white font-bold">Wróć do sklepu</button>
</Link>
</div>
</main>
);
}
const allSpecKeys = [...new Set(items.flatMap((i) => Object.keys(i.specs)))];
return (
<main className="bg-slate-50 min-h-screen py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between mb-8">
<h1 className="text-2xl font-bold text-primary font-heading">Porównanie produktów</h1>
<Link href="/sklep" className="text-sm text-accent hover:underline">← Wróć do sklepu</Link>
</div>
<div className="overflow-x-auto">
<table className="w-full bg-white rounded-xl border border-border overflow-hidden">
<thead>
<tr className="border-b border-border">
<td className="p-4 w-36 bg-slate-50" />
{items.map((item) => (
<td key={item.slug} className="p-4 text-center align-top border-l border-border">
<div className="relative mx-auto size-24 mb-3">
<Image
src={item.image}
alt={item.name}
fill
className="object-contain"
unoptimized={item.image.startsWith('http')}
/>
</div>
<p className="text-xs text-accent font-semibold uppercase mb-1">{item.brand}</p>
<p className="text-sm font-bold text-primary font-heading leading-snug">{item.name}</p>
{item.price && (
<p className="text-lg font-bold text-primary mt-2">
{item.price.toLocaleString('pl-PL')} zł
</p>
)}
<div className="flex gap-2 mt-3 justify-center">
{item.price && (
<button
onClick={() => addToCart({
slug: item.slug,
name: item.name,
brand: item.brand,
image: item.image,
price: item.price!,
})}
className="flex items-center gap-1 px-3 py-1.5 bg-accent text-white rounded-lg text-xs font-bold hover:bg-accent/90 transition-colors"
>
<ShoppingCart className="size-3" /> Do koszyka
</button>
)}
<button
onClick={() => removeItem(item.slug)}
className="p-1.5 text-text-muted hover:text-primary border border-border rounded-lg transition-colors"
>
<X className="size-3.5" />
</button>
</div>
</td>
))}
</tr>
</thead>
<tbody>
{allSpecKeys.map((key, idx) => (
<tr key={key} className={`border-b border-border ${idx % 2 === 0 ? 'bg-white' : 'bg-slate-50'}`}>
<td className="p-3 text-xs font-medium text-text-muted bg-slate-50 border-r border-border">
{key}
</td>
{items.map((item) => (
<td key={item.slug} className="p-3 text-sm text-primary text-center border-l border-border">
{item.specs[key] || <span className="text-border">—</span>}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</main>
);
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { StatsBar } from '@/components/sections/StatsBar';
import { CtaBanner } from '@/components/sections/CtaBanner';
export const metadata: Metadata = {
title: 'Realizacje | AirProject',
title: 'Realizacje | ThermCool',
description: 'Zobacz nasze wybrane realizacje z zakresu klimatyzacji, pomp ciepła, remontów i usług hydraulicznych.',
alternates: {
canonical: '/realizacje',
+12 -6
View File
@@ -1,7 +1,7 @@
import { MetadataRoute } from 'next';
import { getProducts } from '@/content/products';
import { getPosts } from '@/content/posts';
import { dolnySlaskCities } from '@/content/cities';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://thermcool.pl';
@@ -10,16 +10,14 @@ export default function sitemap(): MetadataRoute.Sitemap {
'',
'/klimatyzacja',
'/pompy-ciepla',
'/uslugi-domowe',
'/uslugi-domowe/remonty',
'/uslugi-domowe/hydraulika',
'/uslugi-domowe/serwis-agd',
'/realizacje',
'/blog',
'/o-nas',
'/kontakt',
'/sklep',
'/koszyk',
'/porownaj',
'/regulamin',
'/polityka-prywatnosci',
'/reklamacje',
@@ -50,5 +48,13 @@ export default function sitemap(): MetadataRoute.Sitemap {
priority: 0.7,
}));
return [...staticRoutes, ...productRoutes, ...postRoutes];
// Dynamiczne strony miast
const cityRoutes = dolnySlaskCities.map((city) => ({
url: `${baseUrl}/klimatyzacja/${city.slug}`,
lastModified: new Date().toISOString(),
changeFrequency: 'monthly' as const,
priority: 0.8,
}));
return [...staticRoutes, ...productRoutes, ...postRoutes, ...cityRoutes];
}
+104 -119
View File
@@ -1,149 +1,134 @@
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 Image from 'next/image';
import Link from 'next/link';
import { Phone, ArrowLeft, ShoppingBag } from 'lucide-react';
import { getProductBySlug, getProducts } from '@/content/products';
import { CtaBanner } from '@/components/sections/CtaBanner';
import { companyDetails } from '@/content/company';
interface ProductPageProps {
params: {
slug: string;
};
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
const products = getProducts();
return products.map((product) => ({
slug: product.slug,
}));
return getProducts().map(p => ({ slug: p.slug }));
}
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const product = getProductBySlug(slug);
if (!product) {
return { title: 'Produkt nie znaleziony' };
}
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',
},
],
},
title: `${product.brand} ${product.name} | Sklep ThermCool`,
description: product.shortDescription || `Klimatyzator ${product.name} — sprawdź specyfikację i cenę. Profesjonalny montaż na Dolnym Śląsku.`,
alternates: { canonical: `/sklep/${product.slug}` },
};
}
export default async function ProductPage({ params }: ProductPageProps) {
export default async function ProductPage({ params }: Props) {
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://thermcool.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://thermcool.pl/sklep',
},
{
'@type': 'ListItem',
position: 2,
name: product.name,
item: `https://thermcool.pl/sklep/${product.slug}`,
},
],
};
if (!product) notFound();
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) }}
/>
<main className="bg-slate-50 min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<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" />
{/* Breadcrumb */}
<Link href="/sklep" className="inline-flex items-center gap-2 text-sm text-text-muted hover:text-accent transition-colors mb-6">
<ArrowLeft className="size-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>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Zakładki z detalami */}
<Tabs product={product} />
{/* Zdjęcie */}
<div className="relative aspect-square bg-white rounded-2xl border border-border overflow-hidden">
<Image
src={product.image}
alt={product.name}
fill
className="object-contain p-8"
priority
unoptimized={product.image.startsWith('http')}
/>
</div>
{/* 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} />
))}
{/* Info */}
<div className="flex flex-col gap-6">
<div>
<span className="text-sm font-semibold text-accent uppercase tracking-wider">{product.brand}</span>
<h1 className="text-2xl md:text-3xl font-bold text-primary font-heading mt-1 leading-tight">
{product.name}
</h1>
<p className="text-xs text-text-muted mt-1">SKU: {product.sku}</p>
</div>
</section>
)}
{product.shortDescription && (
<p className="text-text-muted leading-relaxed">{product.shortDescription}</p>
)}
{/* Cena / CTA */}
<div className="p-5 rounded-xl bg-white border border-border">
{product.price !== null ? (
<div>
<p className="text-3xl font-bold text-primary font-heading">
{product.price.toLocaleString('pl-PL')} zł
</p>
<p className="text-sm text-text-muted mt-1">cena urządzenia brutto (23% VAT)</p>
<p className="text-xs text-text-muted mt-1">Montaż wyceniamy indywidualnie — zadzwoń lub napisz</p>
<div className="flex flex-col sm:flex-row gap-3 mt-4">
<a href={`tel:${companyDetails.phoneRaw}`} className="btn btn-primary font-bold text-white flex items-center gap-2 justify-center">
<Phone className="size-4" />
Zadzwoń i zamów
</a>
<Link href="/kontakt" className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 justify-center">
<ShoppingBag className="size-4" />
Zapytaj o montaż
</Link>
</div>
</div>
) : (
<div>
<p className="text-lg font-semibold text-primary font-heading">Cena na zapytanie</p>
<p className="text-sm text-text-muted mt-1">Multisplit — cena zależy od wybranych jednostek wewnętrznych i konfiguracji instalacji.</p>
<a href={`tel:${companyDetails.phoneRaw}`} className="btn btn-primary font-bold text-white flex items-center gap-2 mt-4">
<Phone className="size-4" />
Zapytaj o wycenę
</a>
</div>
)}
</div>
{/* Specyfikacja */}
{Object.keys(product.specs).length > 0 && (
<div>
<h2 className="text-base font-bold text-primary font-heading mb-3">Specyfikacja</h2>
<div className="rounded-xl border border-border overflow-hidden">
<table className="w-full text-sm">
<tbody>
{Object.entries(product.specs).map(([key, val], idx) => (
<tr key={key} className={idx % 2 === 0 ? 'bg-white' : 'bg-slate-50'}>
<td className="px-4 py-2.5 text-text-muted font-medium w-1/2">{key}</td>
<td className="px-4 py-2.5 text-primary">{val}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
</div>
<CtaBanner
title="Potrzebujesz montażu?"
subtitle="Sprzedajemy z montażem na terenie Dolnego Śląska. Zadzwoń i ustal termin."
ctaText="Umów montaż"
ctaLink="/kontakt"
/>
</main>
);
}
+17 -30
View File
@@ -1,46 +1,33 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { getProducts, getBrands } from '@/content/products';
import { ProductsGrid } from '@/components/shop/ProductsGrid';
import { getProducts } from '@/content/products';
import { HeroSection } from '@/components/sections/HeroSection';
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',
}
title: 'Sklep z klimatyzatorami | ThermCool',
description: 'Klimatyzatory split i multisplit — Gree, Rotenso, Haier, Daikin, LG, Samsung i inne. Szeroki wybór, konkurencyjne ceny, profesjonalny montaż.',
alternates: { canonical: '/sklep' },
};
export default function SklepPage() {
export default function ShopPage() {
const products = getProducts();
const brands = getBrands();
const splitCount = products.filter(p => p.category === 'scienne').length;
const multiCount = products.filter(p => p.category === 'multisplit').length;
return (
<main>
<HeroSection
eyebrow="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ą"
eyebrow="Sklep ThermCool"
title="Klimatyzatory split i multisplit"
subtitle={`${splitCount} klimatyzatorów split i ${multiCount} jednostek multisplit. Gree, Rotenso, Haier, Daikin, LG, Samsung i inne marki. Z montażem lub bez.`}
ctaText="Zapytaj o montaż"
ctaLink="/kontakt"
backgroundImage="https://images.unsplash.com/photo-1527689638836-411945a2b57c?w=1600&q=80"
secondaryCtaText={undefined}
secondaryCtaLink={undefined}
backgroundImage="https://images.unsplash.com/photo-1621905251189-08b45d6a269e?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>
<ProductsGrid products={products} brands={brands} />
</main>
);
}
-92
View File
@@ -1,92 +0,0 @@
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
@@ -1,86 +0,0 @@
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
@@ -1,94 +0,0 @@
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
@@ -1,90 +0,0 @@
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>
);
}
+79
View File
@@ -0,0 +1,79 @@
'use server';
import { z } from 'zod';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
const orderSchema = z.object({
name: z.string().min(2),
phone: z.string().min(9),
email: z.string().email(),
address: z.string().optional(),
notes: z.string().optional(),
consent: z.string(),
products: z.string(),
total: z.string(),
});
export async function submitOrderForm(prevState: any, formData: FormData) {
try {
const data = orderSchema.parse({
name: formData.get('name'),
phone: formData.get('phone'),
email: formData.get('email'),
address: formData.get('address') || '',
notes: formData.get('notes') || '',
consent: formData.get('consent'),
products: formData.get('products'),
total: formData.get('total'),
});
const products = JSON.parse(data.products);
const total = parseFloat(data.total);
const productRows = products.map((p: any) =>
`<tr><td style="padding:8px;border-bottom:1px solid #f1f5f9;">${p.name}</td><td style="padding:8px;border-bottom:1px solid #f1f5f9;text-align:center;">${p.quantity}</td><td style="padding:8px;border-bottom:1px solid #f1f5f9;text-align:right;">${(p.price * p.quantity).toLocaleString('pl-PL')} zł</td></tr>`
).join('');
await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL || '[email protected]',
to: process.env.RESEND_TO_EMAIL || '[email protected]',
replyTo: data.email,
subject: `Nowe zamówienie od ${data.name} — ${total.toLocaleString('pl-PL')} zł`,
html: `
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;">
<div style="background:#0F2A47;padding:20px;text-align:center;">
<h1 style="color:#fff;margin:0;font-size:20px;">Nowe zamówienie — ThermCool</h1>
</div>
<div style="padding:24px;">
<h2 style="font-size:16px;margin-top:0;">Dane klienta</h2>
<p><strong>Imię:</strong> ${data.name}</p>
<p><strong>Telefon:</strong> <a href="tel:${data.phone}">${data.phone}</a></p>
<p><strong>Email:</strong> ${data.email}</p>
${data.address ? `<p><strong>Adres:</strong> ${data.address}</p>` : ''}
${data.notes ? `<p><strong>Uwagi:</strong> ${data.notes}</p>` : ''}
<h2 style="font-size:16px;">Zamówione produkty</h2>
<table style="width:100%;border-collapse:collapse;">
<thead><tr style="background:#f8fafc;">
<th style="padding:8px;text-align:left;">Produkt</th>
<th style="padding:8px;text-align:center;">Ilość</th>
<th style="padding:8px;text-align:right;">Wartość</th>
</tr></thead>
<tbody>${productRows}</tbody>
<tfoot><tr>
<td colspan="2" style="padding:8px;font-weight:bold;">Łącznie (urządzenia)</td>
<td style="padding:8px;font-weight:bold;text-align:right;">${total.toLocaleString('pl-PL')} zł</td>
</tr></tfoot>
</table>
<p style="margin-top:16px;font-size:12px;color:#64748b;">+ koszt montażu do ustalenia telefonicznie</p>
</div>
</div>
`,
});
return { success: true, message: 'Dziękujemy! Skontaktujemy się z Tobą w ciągu 24 godzin, aby potwierdzić zamówienie i ustalić termin montażu.' };
} catch (e) {
return { success: false, message: 'Wystąpił błąd. Spróbuj ponownie lub zadzwoń do nas bezpośrednio.' };
}
}
+117 -29
View File
@@ -1,37 +1,125 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { ContactSection } from '@/components/sections/ContactSection';
'use client';
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',
}
};
import { useCartStore } from '@/lib/cart-store';
import { useActionState, useEffect } from 'react';
import Link from 'next/link';
import { submitOrderForm } from './actions';
import { ArrowLeft, CheckCircle } from 'lucide-react';
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"
/>
const initialState = { success: false, message: '' };
<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>
export default function ZamowieniePage() {
const { items, getTotal, clearCart } = useCartStore();
const [state, formAction, pending] = useActionState(submitOrderForm, initialState);
const total = getTotal();
useEffect(() => {
if (state.success) clearCart();
}, [state.success]);
if (items.length === 0 && !state.success) {
return (
<main className="min-h-screen bg-slate-50 flex items-center justify-center">
<div className="text-center">
<p className="text-text-muted mb-4">Koszyk jest pusty.</p>
<Link href="/sklep"><button className="btn btn-primary text-white font-bold">Wróć do sklepu</button></Link>
</div>
</main>
);
}
<ContactSection />
if (state.success) {
return (
<main className="min-h-screen bg-slate-50 flex items-center justify-center">
<div className="text-center max-w-md px-4">
<CheckCircle className="size-16 text-green-500 mx-auto mb-4" />
<h1 className="text-2xl font-bold text-primary font-heading mb-2">Zamówienie przyjęte!</h1>
<p className="text-text-muted mb-6">{state.message}</p>
<Link href="/sklep"><button className="btn btn-primary text-white font-bold">Wróć do sklepu</button></Link>
</div>
</main>
);
}
return (
<main className="bg-slate-50 min-h-screen py-8">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<Link href="/koszyk" className="inline-flex items-center gap-2 text-sm text-text-muted hover:text-accent mb-6">
<ArrowLeft className="size-4" /> Wróć do koszyka
</Link>
<h1 className="text-2xl font-bold text-primary font-heading mb-8">Złóż zamówienie</h1>
<div className="grid grid-cols-1 gap-6">
{/* Podsumowanie produktów */}
<div className="bg-white rounded-xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4">Zamawiane produkty</h2>
<div className="flex flex-col gap-2">
{items.map((item) => (
<div key={item.id} className="flex justify-between text-sm">
<span className="text-text-muted">{item.name} × {item.quantity}</span>
<span className="font-semibold text-primary">{(item.price * item.quantity).toLocaleString('pl-PL')} zł</span>
</div>
))}
<div className="border-t border-border pt-2 mt-2 flex justify-between font-bold text-primary">
<span>Łącznie (urządzenia)</span>
<span>{total.toLocaleString('pl-PL')} zł</span>
</div>
</div>
</div>
{/* Formularz */}
<form action={formAction} className="bg-white rounded-xl border border-border p-6 flex flex-col gap-4">
<h2 className="text-base font-bold text-primary font-heading">Twoje dane</h2>
{/* Hidden: produkty jako JSON */}
<input type="hidden" name="products" value={JSON.stringify(items)} />
<input type="hidden" name="total" value={total} />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Imię i nazwisko *</label>
<input name="name" required className="w-full border border-border rounded-lg px-3 py-2.5 text-sm focus:border-accent outline-none" placeholder="Jan Kowalski" />
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Telefon *</label>
<input name="phone" required type="tel" className="w-full border border-border rounded-lg px-3 py-2.5 text-sm focus:border-accent outline-none" placeholder="+48 600 000 000" />
</div>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Email *</label>
<input name="email" required type="email" className="w-full border border-border rounded-lg px-3 py-2.5 text-sm focus:border-accent outline-none" placeholder="[email protected]" />
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Adres dostawy / montażu</label>
<input name="address" className="w-full border border-border rounded-lg px-3 py-2.5 text-sm focus:border-accent outline-none" placeholder="ul. Przykładowa 1, 50-000 Wrocław" />
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Uwagi do zamówienia</label>
<textarea name="notes" rows={3} className="w-full border border-border rounded-lg px-3 py-2.5 text-sm focus:border-accent outline-none resize-none" placeholder="Np. preferowany termin montażu, piętro, specjalne wymagania..." />
</div>
<label className="flex items-start gap-3 cursor-pointer">
<input type="checkbox" name="consent" required className="mt-0.5 accent-accent" />
<span className="text-xs text-text-muted">Wyrażam zgodę na przetwarzanie danych osobowych w celu realizacji zamówienia zgodnie z Polityką Prywatności. *</span>
</label>
{state.message && !state.success && (
<p className="text-sm text-red-500">{state.message}</p>
)}
<button type="submit" disabled={pending} className="btn btn-primary text-white font-bold w-full flex items-center justify-center gap-2">
{pending ? 'Wysyłanie...' : 'Wyślij zamówienie'}
</button>
<p className="text-xs text-text-muted text-center">
Po złożeniu zamówienia skontaktujemy się z Tobą telefonicznie w ciągu 24 godzin, aby potwierdzić szczegóły i ustalić termin montażu.
</p>
</form>
</div>
</div>
</main>
);