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>
);
+1 -1
View File
@@ -29,7 +29,7 @@ export function SectionHeading({
<span
className={cn(
"text-xs md:text-sm font-semibold uppercase tracking-wider font-heading",
dark ? "text-accent" : "text-cta"
"text-accent"
)}
>
{eyebrow}
+2 -1
View File
@@ -6,6 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
import { submitContactForm } from '@/app/kontakt/actions';
import { companyDetails } from "@/content/company";
const contactFormSchema = z.object({
name: z.string().min(2, "Imię musi mieć min. 2 znaki"),
@@ -170,7 +171,7 @@ export function ContactForm() {
/>
</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ę ThermCool. Zapoznałem/am się z Polityką Prywatności.
Wyrażam zgodę na przetwarzanie moich danych osobowych podanych w formularzu w celu obsługi zapytania przez firmę {companyDetails.shortName}. 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>}
+3 -22
View File
@@ -20,7 +20,7 @@ export function Footer() {
</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.
Profesjonalne usługi w zakresie klimatyzacji i pomp ciepła dla domów, mieszkań i firm.
</p>
<div className="flex flex-col gap-3 mt-2 text-sm text-slate-300">
<a
@@ -70,21 +70,7 @@ export function Footer() {
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
@@ -143,14 +129,9 @@ export function Footer() {
Obsługa Klienta
</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
Sklep online
</Link>
</li>
<li>
+2 -2
View File
@@ -65,10 +65,10 @@ export function GlobalSearch() {
<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>
<span className="text-xs text-text-muted mt-0.5">{product.specs?.['Moc chłodnicza'] ? `${product.specs['Moc chłodnicza']} kW • ` : ''}{product.category}</span>
</div>
<div className="text-right">
<span className="text-sm font-bold text-primary block">{formatCurrency(product.price)}</span>
<span className="text-sm font-bold text-primary block">{product.price !== null ? formatCurrency(product.price) : 'Zapytaj o cenę'}</span>
</div>
</Link>
))}
+20 -53
View File
@@ -71,45 +71,24 @@ export function Header() {
},
{
label: "Serwis i czyszczenie",
href: "/klimatyzacja",
href: "/klimatyzacja#serwis",
description: "Odgrzybianie, naprawy i przeglądy",
},
],
},
{
title: "Montaż w Twoim mieście",
items: [
{ label: "Klimatyzacja Wrocław", href: "/klimatyzacja/wroclaw", description: "Montaż i serwis we Wrocławiu" },
{ label: "Klimatyzacja Legnica", href: "/klimatyzacja/legnica", description: "Obsługujemy Legnicę i okolice" },
{ label: "Klimatyzacja Wałbrzych", href: "/klimatyzacja/walbrzych", description: "Montaż w Wałbrzychu" },
{ label: "Klimatyzacja Jelenia Góra", href: "/klimatyzacja/jelenia-gora", description: "Dolny Śląsk — południe" },
{ label: "→ Wszystkie miasta", href: "/klimatyzacja#miasta", description: "Cały Dolny Śląsk" },
],
},
];
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 (
<>
@@ -123,7 +102,7 @@ export function Header() {
<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
Klimatyzacja i pompy ciepła
</span>
</Link>
@@ -139,15 +118,10 @@ export function Header() {
<div className="flex items-center gap-1">
<GlobalSearch />
<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`}
>
<Link href="/koszyk" className="p-2 text-primary hover:text-accent hover:bg-background rounded-lg transition-colors relative outline-none" aria-label="Koszyk">
<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">
<span className="absolute top-0 right-0 bg-accent 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>
)}
@@ -188,11 +162,7 @@ export function Header() {
>
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"
@@ -246,18 +216,15 @@ export function Header() {
{/* 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" />
<Link href="/koszyk" className="p-2 text-primary hover:text-accent hover:bg-muted transition-colors rounded-lg relative outline-none" aria-label="Koszyk">
<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">
<span className="absolute top-0 right-0 bg-accent 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"
+2 -28
View File
@@ -35,12 +35,7 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
{ 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
@@ -116,28 +111,7 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
</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">
+1 -1
View File
@@ -55,7 +55,7 @@ export function NavDropdown({ label, triggerHref, sections, align = "center" }:
{/* Dropdown Panel */}
<div
className={cn(
"absolute top-full pt-2 z-50 w-[480px] transition-all duration-200 origin-top",
"absolute top-full pt-2 z-50 w-[640px] transition-all duration-200 origin-top",
align === "left" && "left-0",
align === "center" && "left-1/2 -translate-x-1/2",
align === "right" && "right-0",
-121
View File
@@ -1,121 +0,0 @@
'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>
);
};
+3 -3
View File
@@ -15,18 +15,18 @@ export interface HeroSectionProps {
}
export function HeroSection({
eyebrow = "HVAC & Usługi Wykończeniowe",
eyebrow = "Klimatyzacja i Pompy Ciepła",
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.",
subtitle = "Oferujemy profesjonalny dobór, czysty montaż i sprawny serwis klimatyzacji oraz pomp ciepła. Solidnie, estetycznie i z pełną gwarancją.",
ctaText = "Darmowa wycena",
ctaLink = "/kontakt",
secondaryCtaText = "Zobacz ofertę",
secondaryCtaLink = "/uslugi-domowe",
secondaryCtaLink = "/sklep",
backgroundImage = "/hero.jpg",
features
}: HeroSectionProps = {}) {
+10 -8
View File
@@ -47,11 +47,8 @@ export function ProcessSteps({
{/* 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">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-y-12 gap-x-8 relative z-10">
{steps.map((step, idx) => {
const Icon = iconMap[step.icon || ""] || HelpCircle;
return (
@@ -59,10 +56,15 @@ export function ProcessSteps({
{/* 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" />
{step.icon && iconMap[step.icon] && (
<span className="absolute -top-1.5 -right-1.5 size-6 rounded-full bg-accent text-white text-xs font-bold flex items-center justify-center font-heading border-2 border-white shadow-sm">
{step.number}
</span>
)}
{step.icon && iconMap[step.icon]
? <Icon className="size-8 group-hover:scale-110 transition-transform duration-300" />
: <span className="text-2xl font-extrabold font-heading text-accent">{step.number}</span>
}
</div>
{/* Title & Description */}
-2
View File
@@ -13,8 +13,6 @@ 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() {
+1 -1
View File
@@ -9,7 +9,7 @@ export function Testimonials() {
<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."
subtitle="Zadowolenie klientów to nasz najwyższy cel. Zobacz opinie osób, które zaufały naszym montażystom i serwisantom."
align="center"
/>
+4 -4
View File
@@ -5,8 +5,8 @@ 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.",
title: "Doświadczenie i certyfikaty",
description: "Posiadamy wszystkie wymagane certyfikaty i autoryzacje. Działamy na rynku od ponad 15 lat.",
},
{
icon: FileText,
@@ -32,8 +32,8 @@ export function WhyUs() {
{/* 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 ThermCool
<span className="text-xs md:text-sm font-semibold uppercase tracking-wider text-accent font-heading">
{`Dlaczego ${companyDetails.shortName}`}
</span>
<h2 className="text-3xl md:text-4xl font-bold text-primary font-heading leading-tight mt-2">
Zaufany partner w instalacjach domowych
+4 -25
View File
@@ -5,10 +5,10 @@ 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';
import { Minus, Plus, Trash2 } from 'lucide-react';
export function CartItems() {
const { items, removeItem, updateQuantity, toggleInstall } = useCartStore();
const { items, removeItem, updateQuantity } = useCartStore();
if (items.length === 0) {
return (
@@ -28,7 +28,7 @@ export function CartItems() {
return (
<div className="space-y-6">
{items.map((item) => {
const itemPrice = item.withInstall ? item.priceWithInstall : item.price;
const itemPrice = item.price;
const totalGross = itemPrice * item.quantity;
return (
@@ -63,31 +63,10 @@ export function CartItems() {
{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>
<div className="flex flex-col sm:flex-row sm:items-center justify-end gap-4 mt-4">
{/* Ilość i Cena */}
<div className="flex items-center gap-6">
<div className="flex items-center border border-border rounded-lg bg-slate-50">
+6 -12
View File
@@ -7,15 +7,14 @@ import Link from 'next/link';
import { ShieldCheck, ArrowRight, Tag } from 'lucide-react';
export function CartSummary() {
const { items, getTotals } = useCartStore();
const { items, getTotal } = 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;
const total = getTotal();
const subtotal = total / 1.23;
const vat = total - subtotal;
return (
<div className="bg-slate-50 p-6 md:p-8 rounded-2xl border border-border sticky top-24">
@@ -47,14 +46,9 @@ export function CartSummary() {
<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>Podatek VAT (23%)</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 */}
@@ -63,7 +57,7 @@ export function CartSummary() {
<span className="text-3xl font-extrabold text-primary font-heading text-right">{formatCurrency(total)}</span>
</div>
{/* Przycisk (Placeholder - brak właściwego checkoutu) */}
{/* Przycisk */}
<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
+64
View File
@@ -0,0 +1,64 @@
'use client';
import { useCompareStore } from '@/lib/compare-store';
import { X, GitCompare } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
export function CompareBar() {
const { items, removeItem, clearAll } = useCompareStore();
if (items.length === 0) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-border shadow-lg">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 shrink-0">
<GitCompare className="size-4 text-accent" />
<span className="text-sm font-semibold text-primary hidden sm:block">
Porównaj ({items.length}/3)
</span>
</div>
<div className="flex items-center gap-3 flex-1 overflow-x-auto">
{items.map((item) => (
<div key={item.slug} className="flex items-center gap-2 bg-slate-50 rounded-lg px-3 py-1.5 border border-border shrink-0">
<div className="relative size-8 bg-white rounded">
<Image src={item.image} alt={item.name} fill className="object-contain p-0.5" unoptimized={item.image.startsWith('http')} />
</div>
<div className="hidden sm:block">
<p className="text-xs font-medium text-primary line-clamp-1 max-w-[140px]">{item.name}</p>
{item.price && <p className="text-xs text-accent font-bold">{item.price.toLocaleString('pl-PL')} zł</p>}
</div>
<button onClick={() => removeItem(item.slug)} className="text-text-muted hover:text-primary transition-colors ml-1">
<X className="size-3.5" />
</button>
</div>
))}
{/* Puste sloty */}
{Array.from({ length: 3 - items.length }).map((_, i) => (
<div key={i} className="size-11 rounded-lg border-2 border-dashed border-border flex items-center justify-center text-text-muted shrink-0">
<span className="text-lg">+</span>
</div>
))}
</div>
<div className="flex items-center gap-2 shrink-0">
{items.length >= 2 && (
<Link href="/porownaj">
<button className="btn btn-primary text-white font-bold text-sm py-2 px-4">
Porównaj
</button>
</Link>
)}
<button onClick={clearAll} className="text-xs text-text-muted hover:text-primary transition-colors">
Wyczyść
</button>
</div>
</div>
</div>
</div>
);
}
+135 -55
View File
@@ -1,80 +1,160 @@
import React from 'react';
'use client';
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';
import { ShoppingCart, GitCompare, Phone, Check } from 'lucide-react';
import { Product } from '@/content/products';
import { useCartStore } from '@/lib/cart-store';
import { useCompareStore } from '@/lib/compare-store';
import { useState } from 'react';
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;
const addItem = useCartStore((s) => s.addItem);
const { addItem: addCompare, removeItem: removeCompare, isAdded, items: compareItems } = useCompareStore();
const [added, setAdded] = useState(false);
const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault();
if (!product.price) return;
addItem({
slug: product.slug,
name: product.name,
brand: product.brand,
image: product.image,
price: product.price,
});
setAdded(true);
setTimeout(() => setAdded(false), 1500);
};
const handleCompare = (e: React.MouseEvent) => {
e.preventDefault();
if (isAdded(product.slug)) {
removeCompare(product.slug);
} else {
addCompare({
slug: product.slug,
name: product.name,
brand: product.brand,
price: product.price,
image: product.image,
specs: product.specs,
});
}
};
const inCompare = isAdded(product.slug);
const compareDisabled = compareItems.length >= 3 && !inCompare;
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>
<div className="group relative bg-white rounded-xl border border-border hover:border-accent hover:shadow-md transition-all duration-200 overflow-hidden flex flex-col">
{/* Zdjęcie */}
<Link href={`/sklep/${product.slug}`} className="block relative aspect-[4/3] bg-slate-50 overflow-hidden">
<Image
src={product.image || 'https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80'}
src={product.image}
alt={product.name}
width={400}
height={400}
className="object-contain w-full h-full transition-transform duration-500 group-hover:scale-105"
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-contain p-6 group-hover:scale-105 transition-transform duration-300"
unoptimized={product.image.startsWith('http')}
/>
{/* Badge marka */}
<span className="absolute top-2 left-2 px-2 py-1 bg-white/95 rounded-md text-xs font-semibold text-primary border border-border shadow-sm">
{product.brand}
</span>
{/* Badge multisplit */}
{product.category === 'multisplit' && (
<span className="absolute top-2 right-2 px-2 py-1 bg-accent/10 rounded-md text-xs font-semibold text-accent border border-accent/20">
Multisplit
</span>
)}
{/* Porównywarka - pojawia się na hover */}
<button
onClick={handleCompare}
disabled={compareDisabled}
title={inCompare ? 'Usuń z porównywarki' : compareDisabled ? 'Max 3 produkty' : 'Dodaj do porównania'}
className={`absolute bottom-2 right-2 p-1.5 rounded-lg border transition-all ${
inCompare
? 'bg-accent text-white border-accent opacity-100'
: 'bg-white/90 text-text-muted border-border opacity-0 group-hover:opacity-100 hover:border-accent hover:text-accent'
} ${compareDisabled ? 'opacity-30 cursor-not-allowed' : ''}`}
>
<GitCompare className="size-3.5" />
</button>
</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}`}>
<div className="p-4 flex flex-col flex-1 gap-2">
<Link href={`/sklep/${product.slug}`}>
<h3 className="text-sm font-semibold text-primary font-heading leading-snug line-clamp-2 hover:text-accent transition-colors">
{product.name}
</Link>
</h3>
</h3>
</Link>
{/* 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>
{/* Kluczowe specs */}
<div className="flex flex-wrap gap-1.5">
{product.specs['Moc chłodnicza'] && (
<span className="px-2 py-0.5 bg-slate-50 rounded text-xs text-text-muted border border-border">
{product.specs['Moc chłodnicza']} kW
</span>
)}
{product.specs['Klasa energetyczna'] && (
<span className="px-2 py-0.5 bg-green-50 rounded text-xs text-green-700 border border-green-100 font-medium">
{product.specs['Klasa energetyczna']}
</span>
)}
{product.specs['Czynnik chłodniczy'] && (
<span className="px-2 py-0.5 bg-slate-50 rounded text-xs text-text-muted border border-border">
{product.specs['Czynnik chłodniczy']}
</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>
{/* Cena + CTA */}
<div className="mt-auto pt-2 flex items-end justify-between gap-2">
{product.price !== null ? (
<div>
<p className="text-lg font-bold text-primary font-heading leading-none">
{product.price.toLocaleString('pl-PL')} zł
</p>
<p className="text-xs text-text-muted mt-0.5">brutto</p>
</div>
) : (
<div className="flex items-center gap-1 text-accent">
<Phone className="size-3.5" />
<span className="text-sm font-semibold">Zapytaj o cenę</span>
</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'}
{product.price !== null ? (
<button
onClick={handleAddToCart}
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-xs font-bold transition-all shrink-0 ${
added
? 'bg-green-500 text-white'
: 'bg-accent text-white hover:bg-accent/90'
}`}
>
{added ? (
<><Check className="size-3.5" /> Dodano</>
) : (
<><ShoppingCart className="size-3.5" /> Do koszyka</>
)}
</button>
</Link>
) : (
<Link href="/kontakt">
<button className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-xs font-bold bg-primary text-white hover:bg-primary/90 transition-colors shrink-0">
Zapytaj
</button>
</Link>
)}
</div>
</div>
</div>
-276
View File
@@ -1,276 +0,0 @@
"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>
);
}
+502 -142
View File
@@ -1,160 +1,520 @@
"use client";
'use client';
import React, { useState, useMemo } from 'react';
import { Product } from '@/types/types';
import { useState, useMemo } from 'react';
import { ChevronDown, ChevronUp, Phone, X } from 'lucide-react';
import Link from 'next/link';
import { Product } from '@/content/products';
import { ProductCard } from './ProductCard';
import { SlidersHorizontal, X, Search } from 'lucide-react';
import { companyDetails } from '@/content/company';
interface ProductsGridProps {
products: Product[];
brands: string[];
}
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);
const PER_PAGE = 24;
// Pobranie unikalnych wartości do filtrów
const brands = useMemo(() => Array.from(new Set(products.map(p => p.brand))), [products]);
// Parsuj moc chłodniczą — zwróć wartość MAX z zakresu
function parseMocMax(val: string): number | null {
if (!val) return null;
const nums = val.replace(/[^\d,.-]/g, ' ').split(/[\s\/–-]+/).map(s => parseFloat(s.replace(',', '.'))).filter(n => !isNaN(n));
return nums.length > 0 ? Math.max(...nums) : null;
}
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]);
// Parsuj hałas — zwróć wartość MIN z zakresu
function parseHalasMin(val: string): number | null {
if (!val) return null;
const nums = val.match(/\d+/g);
return nums ? Math.min(...nums.map(Number)) : null;
}
// Logika filtrowania i sortowania
const filteredProducts = useMemo(() => {
let result = [...products];
// Mapowanie wykończenia na grupy kolorystyczne
function mapWykonczenie(val: string): string {
const v = val.toLowerCase();
if (v.includes('white') || v === 'biały') return 'Biały';
if (v.includes('black') || v === 'czarny' || v === 'dark' || v.includes('satin')) return 'Czarny';
if (v.includes('silver') || v === 'srebrny') return 'Srebrny';
if (v.includes('gold') || v === 'złoty' || v === 'champagne') return 'Złoty';
if (v.includes('graphite') || v.includes('grey') || v.includes('gray')) return 'Grafitowy';
if (v.includes('wood') || v.includes('fabric') || v.includes('textile') || v.includes('leather') || v.includes('stone') || v.includes('mirror') || v.includes('caramel') || v.includes('navy') || v.includes('miedziany') || v === 'beige stone') return 'Inne';
if (v === 'matt' || v === 'shiny') return 'Biały';
return 'Inne';
}
if (selectedBrands.length > 0) {
result = result.filter(p => selectedBrands.includes(p.brand));
}
if (selectedCategories.length > 0) {
result = result.filter(p => selectedCategories.includes(p.category));
}
const COLOR_GROUPS = ['Biały', 'Czarny', 'Srebrny', 'Złoty', 'Grafitowy', 'Inne'];
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);
}
// Buckety wielkości pomieszczenia → zakres mocy kW
const ROOM_BUCKETS = [
{ label: 'do 20 m²', min: 0, max: 2.5 },
{ label: '21–35 m²', min: 2.5, max: 3.5 },
{ label: '36–50 m²', min: 3.5, max: 5.0 },
{ label: '51–70 m²', min: 5.0, max: 7.0 },
{ label: 'powyżej 70 m²', min: 7.0, max: 99 },
];
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]
);
};
interface AccordionProps {
title: string;
children: React.ReactNode;
defaultOpen?: boolean;
}
function Accordion({ title, children, defaultOpen = true }: AccordionProps) {
const [open, setOpen] = useState(defaultOpen);
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 className="border-b border-border pb-4 mb-4">
<button
onClick={() => setOpen(!open)}
className="flex items-center justify-between w-full text-left"
>
<span className="text-sm font-bold text-primary font-heading">{title}</span>
{open ? <ChevronUp className="size-4 text-text-muted" /> : <ChevronDown className="size-4 text-text-muted" />}
</button>
{open && <div className="mt-3">{children}</div>}
</div>
);
}
export function ProductsGrid({ products, brands }: ProductsGridProps) {
const [category, setCategory] = useState<'all' | 'scienne' | 'multisplit'>('all');
const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
const [priceMin, setPriceMin] = useState('');
const [priceMax, setPriceMax] = useState('');
const [selectedRooms, setSelectedRooms] = useState<string[]>([]);
const [selectedColors, setSelectedColors] = useState<string[]>([]);
const [halasMin, setHalasMin] = useState(15);
const [halasMax, setHalasMax] = useState(50);
const [page, setPage] = useState(1);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const toggleBrand = (b: string) => {
setSelectedBrands(prev => prev.includes(b) ? prev.filter(x => x !== b) : [...prev, b]);
setPage(1);
};
const toggleRoom = (r: string) => {
setSelectedRooms(prev => prev.includes(r) ? prev.filter(x => x !== r) : [...prev, r]);
setPage(1);
};
const toggleColor = (c: string) => {
setSelectedColors(prev => prev.includes(c) ? prev.filter(x => x !== c) : [...prev, c]);
setPage(1);
};
const filtered = useMemo(() => {
return products.filter(p => {
// Kategoria
if (category !== 'all' && p.category !== category) return false;
// Marka
if (selectedBrands.length > 0 && !selectedBrands.includes(p.brand)) return false;
// Cena
const pMin = priceMin ? parseInt(priceMin) : null;
const pMax = priceMax ? parseInt(priceMax) : null;
if (p.price !== null) {
if (pMin && p.price < pMin) return false;
if (pMax && p.price > pMax) return false;
}
// Wielkość pomieszczenia → moc
if (selectedRooms.length > 0) {
const moc = parseMocMax(p.specs['Moc chłodnicza'] || '');
if (moc === null) return false;
const matches = selectedRooms.some(r => {
const bucket = ROOM_BUCKETS.find(b => b.label === r);
return bucket ? moc >= bucket.min && moc < bucket.max : false;
});
if (!matches) return false;
}
// Hałas
if (halasMin > 15 || halasMax < 50) {
const h = parseHalasMin(p.specs['Hałas wewnątrz (dB)'] || '');
if (h !== null && (h < halasMin || h > halasMax)) return false;
}
// Kolor
if (selectedColors.length > 0) {
const wyk = p.specs['Wykończenie'] || '';
const group = mapWykonczenie(wyk);
if (!selectedColors.includes(group)) return false;
}
return true;
});
}, [products, category, selectedBrands, priceMin, priceMax, selectedRooms, halasMin, halasMax, selectedColors]);
const totalPages = Math.ceil(filtered.length / PER_PAGE);
const paginated = filtered.slice((page - 1) * PER_PAGE, page * PER_PAGE);
// Liczniki per brand (dla aktualnych filtrów bez filtra marki)
const brandCounts = useMemo(() => {
const counts: Record<string, number> = {};
products.forEach(p => {
if (category !== 'all' && p.category !== category) return;
counts[p.brand] = (counts[p.brand] || 0) + 1;
});
return counts;
}, [products, category]);
// Liczniki per room bucket
const roomCounts = useMemo(() => {
const counts: Record<string, number> = {};
products.forEach(p => {
if (category !== 'all' && p.category !== category) return;
const moc = parseMocMax(p.specs['Moc chłodnicza'] || '');
if (moc === null) return;
ROOM_BUCKETS.forEach(b => {
if (moc >= b.min && moc < b.max) {
counts[b.label] = (counts[b.label] || 0) + 1;
}
});
});
return counts;
}, [products, category]);
const resetAll = () => {
setCategory('all');
setSelectedBrands([]);
setPriceMin('');
setPriceMax('');
setSelectedRooms([]);
setSelectedColors([]);
setHalasMin(15);
setHalasMax(50);
setPage(1);
};
const hasFilters = category !== 'all' || selectedBrands.length > 0 || priceMin || priceMax || selectedRooms.length > 0 || selectedColors.length > 0 || halasMin > 15 || halasMax < 50;
const Sidebar = () => (
<aside className="w-full">
{/* Klimatyzatory - typ */}
<Accordion title="Klimatyzatory">
<ul className="flex flex-col gap-2">
{[
{ val: 'all', label: 'Wszystkie' },
{ val: 'scienne', label: 'Klimatyzacja ścienna' },
{ val: 'multisplit', label: 'Klimatyzacja Multisplit' },
].map(opt => (
<li key={opt.val}>
<button
onClick={() => { setCategory(opt.val as typeof category); setPage(1); }}
className={`flex items-center gap-2.5 text-sm w-full text-left py-0.5 transition-colors ${
category === opt.val ? 'text-primary font-semibold' : 'text-text-muted hover:text-primary'
}`}
>
<span className={`size-3 rounded-full border-2 shrink-0 ${
category === opt.val ? 'border-accent bg-accent' : 'border-border'
}`} />
{opt.label}
<span className="ml-auto text-xs bg-slate-100 rounded-full px-1.5 py-0.5 text-text-muted">
{opt.val === 'all' ? products.length : products.filter(p => p.category === opt.val).length}
</span>
</button>
</li>
))}
</ul>
</Accordion>
{/* Marka */}
<Accordion title="Marka">
<ul className="flex flex-col gap-2">
{brands.map(b => (
<li key={b}>
<label className="flex items-center gap-2.5 cursor-pointer group">
<input
type="checkbox"
checked={selectedBrands.includes(b)}
onChange={() => toggleBrand(b)}
className="sr-only"
/>
<span className={`size-4 rounded border-2 shrink-0 flex items-center justify-center transition-colors ${
selectedBrands.includes(b)
? 'bg-accent border-accent'
: 'bg-white border-slate-300 group-hover:border-accent'
}`}>
{selectedBrands.includes(b) && (
<svg viewBox="0 0 10 8" className="size-2.5" fill="none">
<path d="M1 4L3.5 6.5L9 1" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</span>
<span className={`text-sm transition-colors ${selectedBrands.includes(b) ? 'text-primary font-medium' : 'text-text-muted group-hover:text-primary'}`}>
{b}
</span>
<span className="ml-auto text-xs text-text-muted">
{brandCounts[b] || 0}
</span>
</label>
</li>
))}
</ul>
</Accordion>
{/* Cena */}
<Accordion title="Cena (zł)">
<div className="flex items-center gap-2">
<input
type="number"
placeholder="od"
value={priceMin}
onChange={e => { setPriceMin(e.target.value); setPage(1); }}
className="w-full border border-border rounded-lg px-3 py-2 text-sm text-primary focus:border-accent outline-none"
/>
<span className="text-text-muted shrink-0">–</span>
<input
type="number"
placeholder="do"
value={priceMax}
onChange={e => { setPriceMax(e.target.value); setPage(1); }}
className="w-full border border-border rounded-lg px-3 py-2 text-sm text-primary focus:border-accent outline-none"
/>
</div>
</Accordion>
{/* Wielkość pomieszczenia */}
<Accordion title="Wielkość pomieszczenia">
<ul className="flex flex-col gap-2">
{ROOM_BUCKETS.map(b => (
<li key={b.label}>
<label className="flex items-center gap-2.5 cursor-pointer group">
<input
type="checkbox"
checked={selectedRooms.includes(b.label)}
onChange={() => toggleRoom(b.label)}
className="sr-only"
/>
<span className={`size-4 rounded border-2 shrink-0 flex items-center justify-center transition-colors ${
selectedRooms.includes(b.label)
? 'bg-accent border-accent'
: 'bg-white border-slate-300 group-hover:border-accent'
}`}>
{selectedRooms.includes(b.label) && (
<svg viewBox="0 0 10 8" className="size-2.5" fill="none">
<path d="M1 4L3.5 6.5L9 1" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</span>
<span className={`text-sm transition-colors ${selectedRooms.includes(b.label) ? 'text-primary font-medium' : 'text-text-muted group-hover:text-primary'}`}>
{b.label}
</span>
<span className="ml-auto text-xs text-text-muted">
{roomCounts[b.label] || 0}
</span>
</label>
</li>
))}
</ul>
<p className="text-xs text-text-muted mt-2">Szacunkowo: 10 m² = 1 kW mocy chłodniczej</p>
</Accordion>
{/* Głośność */}
<Accordion title={`Głośność wewnętrzna (dB) — ${halasMin}–${halasMax} dB`}>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<span className="text-xs text-text-muted w-8">od</span>
<input
type="range"
min={15}
max={50}
value={halasMin}
onChange={e => {
const val = Number(e.target.value);
setHalasMin(Math.min(val, halasMax - 1));
setPage(1);
}}
className="flex-1 accent-accent cursor-pointer"
/>
<span className="text-xs text-primary font-medium w-10 text-right">{halasMin} dB</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-text-muted w-8">do</span>
<input
type="range"
min={15}
max={50}
value={halasMax}
onChange={e => {
const val = Number(e.target.value);
setHalasMax(Math.max(val, halasMin + 1));
setPage(1);
}}
className="flex-1 accent-accent cursor-pointer"
/>
<span className="text-xs text-primary font-medium w-10 text-right">{halasMax} dB</span>
</div>
</div>
</Accordion>
{/* Kolor/Wykończenie */}
<Accordion title="Kolor dominujący">
<ul className="flex flex-col gap-2">
{COLOR_GROUPS.map(c => (
<li key={c}>
<label className="flex items-center gap-2.5 cursor-pointer group">
<input
type="checkbox"
checked={selectedColors.includes(c)}
onChange={() => toggleColor(c)}
className="sr-only"
/>
<span className={`size-4 rounded border-2 shrink-0 flex items-center justify-center transition-colors ${
selectedColors.includes(c)
? 'bg-accent border-accent'
: 'bg-white border-slate-300 group-hover:border-accent'
}`}>
{selectedColors.includes(c) && (
<svg viewBox="0 0 10 8" className="size-2.5" fill="none">
<path d="M1 4L3.5 6.5L9 1" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</span>
<span className={`text-sm transition-colors ${selectedColors.includes(c) ? 'text-primary font-medium' : 'text-text-muted group-hover:text-primary'}`}>
{c}
</span>
</label>
</li>
))}
</ul>
</Accordion>
{/* Potrzebujesz pomocy */}
<div className="rounded-xl bg-slate-50 border border-border p-4 text-center mt-2">
<p className="text-sm font-bold text-primary font-heading mb-3">Potrzebujesz pomocy?</p>
<a
href={`tel:${companyDetails.phoneRaw}`}
className="flex items-center justify-center gap-2 text-primary font-bold text-base hover:text-accent transition-colors"
>
<Phone className="size-4 text-accent" />
{companyDetails.phone}
</a>
<p className="text-xs text-text-muted mt-1">{companyDetails.hours}</p>
<Link href="/kontakt">
<button className="mt-3 w-full btn btn-primary text-white font-bold text-sm py-2">
Zadaj pytanie
</button>
</Link>
</div>
</aside>
);
return (
<section className="py-8 md:py-12 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Mobile: przycisk filtrów */}
<div className="lg:hidden mb-4 flex items-center justify-between">
<button
onClick={() => setMobileSidebarOpen(true)}
className="flex items-center gap-2 px-4 py-2 border border-border rounded-lg text-sm font-medium text-primary hover:border-accent hover:text-accent transition-colors"
>
<ChevronDown className="size-4" />
Filtry {hasFilters && <span className="size-2 rounded-full bg-accent inline-block" />}
</button>
<span className="text-sm text-text-muted">{filtered.length} produktów</span>
</div>
{/* Mobile sidebar overlay */}
{mobileSidebarOpen && (
<div className="fixed inset-0 z-50 lg:hidden">
<div className="absolute inset-0 bg-black/50" onClick={() => setMobileSidebarOpen(false)} />
<div className="absolute top-0 left-0 bottom-0 w-80 bg-white overflow-y-auto p-6 shadow-xl">
<div className="flex items-center justify-between mb-6">
<span className="font-bold text-primary font-heading">Filtry</span>
<button onClick={() => setMobileSidebarOpen(false)}>
<X className="size-5 text-text-muted" />
</button>
</div>
<Sidebar />
</div>
</div>
)}
<div className="flex gap-8">
{/* Desktop sidebar */}
<div className="hidden lg:block w-64 shrink-0">
<div className="sticky top-24 max-h-[calc(100vh-7rem)] overflow-y-auto pr-2">
{hasFilters && (
<button
onClick={resetAll}
className="flex items-center gap-1.5 text-xs text-accent hover:underline mb-4"
>
<X className="size-3" /> Wyczyść filtry
</button>
)}
<Sidebar />
</div>
</div>
{/* Produkty */}
<div className="flex-1 min-w-0">
{/* Nagłówek z licznikiem */}
<div className="flex items-center justify-between mb-6">
<span className="text-sm text-text-muted hidden lg:block">{filtered.length} produktów</span>
{hasFilters && (
<button onClick={resetAll} className="lg:hidden text-xs text-accent hover:underline flex items-center gap-1">
<X className="size-3" /> Wyczyść
</button>
)}
</div>
{/* Grid */}
{paginated.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-5">
{paginated.map(product => (
<ProductCard key={product.slug} product={product} />
))}
</div>
) : (
<div className="text-center py-24">
<p className="text-text-muted mb-3">Brak produktów dla wybranych filtrów.</p>
<button onClick={resetAll} className="text-accent hover:underline text-sm">
Wyczyść filtry
</button>
</div>
)}
{/* Paginacja */}
{totalPages > 1 && (
<div className="flex justify-center gap-2 mt-12">
<button
onClick={() => { setPage(p => Math.max(1, p - 1)); window.scrollTo(0, 0); }}
disabled={page === 1}
className="px-4 py-2 rounded-lg border border-border text-sm font-medium text-primary hover:border-accent hover:text-accent disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
← Poprzednia
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const p = page <= 3 ? i + 1 : page - 2 + i;
if (p < 1 || p > totalPages) return null;
return (
<button
key={p}
onClick={() => { setPage(p); window.scrollTo(0, 0); }}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
p === page
? 'bg-accent text-white border border-accent'
: 'border border-border text-primary hover:border-accent hover:text-accent'
}`}
>
{p}
</button>
);
})}
<button
onClick={() => { setPage(p => Math.min(totalPages, p + 1)); window.scrollTo(0, 0); }}
disabled={page === totalPages}
className="px-4 py-2 rounded-lg border border-border text-sm font-medium text-primary hover:border-accent hover:text-accent disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
Następna →
</button>
</div>
)}
</div>
</div>
</div>
</section>
);
}
-118
View File
@@ -1,118 +0,0 @@
"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>
);
}
+1 -1
View File
@@ -29,7 +29,7 @@ export function CookieBanner() {
};
const handleOpenSettings = () => {
const hasConsent = localStorage.getItem('airproject_cookie_consent_v2');
const hasConsent = localStorage.getItem('thermcool_cookie_consent_v1');
setLocalAnalytics(hasConsent ? analytics : true);
setLocalMarketing(hasConsent ? marketing : true);
setShowSettings(true);
+313
View File
@@ -0,0 +1,313 @@
export interface CityData {
slug: string;
name: string;
nameDopelniacz: string;
nameDopelniacz2: string;
region: string;
population: string;
distanceFromHQ: string;
lat: number;
lng: number;
heroImage: string;
seoTitle: string;
seoDescription: string;
heroSubtitle: string;
introText: string;
localFacts: string;
acHighlight: string;
heatPumpTeaser: string;
}
export const dolnySlaskCities: CityData[] = [
{
slug: "wroclaw",
name: "Wrocław",
nameDopelniacz: "Wrocławiu",
nameDopelniacz2: "Wrocławia",
region: "Dolny Śląsk",
population: "640 tys.",
distanceFromHQ: "siedziba firmy",
lat: 51.1079,
lng: 17.0385,
heroImage: "https://images.unsplash.com/photo-1585155784229-aff921ccfa22?w=1600&q=80",
seoTitle: "Klimatyzacja Wrocław — Montaż i Serwis | ThermCool",
seoDescription: "Montaż klimatyzacji we Wrocławiu od specjalistów z uprawnieniami F-Gaz. Bezpłatny audyt, split i multisplit, serwis gwarancyjny. Zadzwoń po wycenę!",
heroSubtitle: "Profesjonalny montaż klimatyzacji we Wrocławiu — split, multisplit, kasetonowe. Bezpłatny audyt, estetyczna instalacja, pełna gwarancja.",
introText: "Wrocław to jedno z najszybciej rozwijających się miast w Polsce, gdzie latem temperatury regularnie przekraczają 30°C. Nasze ekipy montażowe działają na terenie całego miasta — od Fabrycznej przez Śródmieście i Krzyki po Psie Pole i Stabłowice. Montujemy klimatyzatory w mieszkaniach deweloperskich, domach jednorodzinnych, biurach i lokalach usługowych.",
localFacts: "Rocznie realizujemy kilkaset instalacji klimatyzacji we Wrocławiu i okolicach. Doskonale znamy specyfikę lokalnego budownictwa — zarówno nowe osiedla jak Południe czy Nowe Żerniki, jak i kamienice w centrum wymagające dyskretnego prowadzenia instalacji.",
acHighlight: "Specjalizujemy się w montażu klimatyzatorów split i multisplit wiodących marek: Daikin, Mitsubishi Electric, Gree, Rotenso. Każdą instalację prowadzimy starannie — ukryte korytka kablowe, precyzyjny przewiert, sprzątanie po montażu.",
heatPumpTeaser: "Planujesz ogrzewanie pompy ciepła we Wrocławiu? Pomagamy w doborze, montażu i uzyskaniu dofinansowania z programu Czyste Powietrze. Realizujemy instalacje powietrze-woda na terenie całego Wrocławia i powiatu wrocławskiego.",
},
{
slug: "legnica",
name: "Legnica",
nameDopelniacz: "Legnicy",
nameDopelniacz2: "Legnicy",
region: "Dolny Śląsk",
population: "96 tys.",
distanceFromHQ: "65 km od Wrocławia",
lat: 51.2070,
lng: 16.1619,
heroImage: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1600&q=80",
seoTitle: "Klimatyzacja Legnica — Montaż i Serwis | ThermCool",
seoDescription: "Montaż klimatyzacji w Legnicy i powiecie legnickim. Certyfikowani monterzy F-Gaz, bezpłatna wycena, szybki termin realizacji. Sprawdź ofertę ThermCool.",
heroSubtitle: "Montaż klimatyzacji w Legnicy — split i multisplit dla domów, mieszkań i firm. Bezpłatny audyt i jasna wycena bez ukrytych kosztów.",
introText: "Legnica to ważny ośrodek miejski Dolnego Śląska, w którym coraz więcej mieszkańców i przedsiębiorców decyduje się na klimatyzację jako standard komfortu. Obsługujemy Legnicę i okoliczne gminy — Prochowice, Kunice, Miłkowice. Dojazd do Legnicy zajmuje nam około godziny, a dzięki planowaniu tras realizujemy kilka zleceń dziennie w tym rejonie.",
localFacts: "W Legnicy obserwujemy rosnące zainteresowanie klimatyzacją wśród właścicieli starszych kamienic w centrum oraz nowych domów jednorodzinnych na obrzeżach. Znamy specyfikę miejscowego budownictwa i dobieramy rozwiązania dopasowane do grubości ścian i układu pomieszczeń.",
acHighlight: "Montujemy klimatyzatory ścienne split od 2,5 do 7 kW oraz systemy multisplit dla wielopokojowych mieszkań i biur. Wszystkie prace wykonujemy zgodnie z wymogami F-Gaz, a każdą instalację kończymy próbą szczelności i szkoleniem z obsługi.",
heatPumpTeaser: "Interesuje Cię pompa ciepła w Legnicy? Dobieramy urządzenia dopasowane do zapotrzebowania budynku, pomagamy uzyskać dofinansowanie z Czystego Powietrza i realizujemy kompletną instalację powietrze-woda.",
},
{
slug: "walbrzych",
name: "Wałbrzych",
nameDopelniacz: "Wałbrzychu",
nameDopelniacz2: "Wałbrzycha",
region: "Dolny Śląsk",
population: "105 tys.",
distanceFromHQ: "70 km od Wrocławia",
lat: 50.7714,
lng: 16.2844,
heroImage: "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?w=1600&q=80",
seoTitle: "Klimatyzacja Wałbrzych — Montaż i Serwis | ThermCool",
seoDescription: "Klimatyzacja Wałbrzych — profesjonalny montaż split i multisplit. Uprawnienia F-Gaz, bezpłatny audyt, dojazd do klienta. Zadzwoń i umów wycenę.",
heroSubtitle: "Klimatyzacja w Wałbrzychu i Kotlinie Kłodzkiej — montaż, serwis i dobór urządzeń dla domów i firm. Ekipa z certyfikatem F-Gaz.",
introText: "Wałbrzych, mimo górskiego położenia, latem bywa równie upalny co nizinne miasta Dolnego Śląska. Obsługujemy mieszkańców Wałbrzycha oraz okoliczne miejscowości — Boguszów-Gorce, Szczawno-Zdrój, Świebodzice. Klimatyzacja w tym regionie to nie tylko chłodzenie latem, ale też ekonomiczne ogrzewanie przez całą zimę.",
localFacts: "W Wałbrzychu realizujemy montaże zarówno w blokach z wielkiej płyty jak i w domach jednorodzinnych na wzgórzach. Lokalizacja wymaga niekiedy dłuższych tras instalacyjnych — nasi monterzy mają doświadczenie w prowadzeniu instalacji przez trudniejsze konstrukcje budowlane.",
acHighlight: "Polecamy rozwiązania z funkcją grzania — klimatyzatory inwerterowe efektywnie ogrzewają pomieszczenia nawet przy -15°C na zewnątrz, co w wałbrzyskim klimacie jest szczególnie wartościowe. Koszty ogrzewania spadają nawet o 60% w porównaniu z tradycyjnym ogrzewaniem elektrycznym.",
heatPumpTeaser: "Pompa ciepła w Wałbrzychu to inwestycja szczególnie opłacalna — wyższe położenie i chłodniejszy klimat sprawiają że urządzenia pracują wydajniej. Pomożemy w doborze, montażu i uzyskaniu dotacji z Czystego Powietrza.",
},
{
slug: "jelenia-gora",
name: "Jelenia Góra",
nameDopelniacz: "Jeleniej Górze",
nameDopelniacz2: "Jeleniej Góry",
region: "Dolny Śląsk",
population: "78 tys.",
distanceFromHQ: "110 km od Wrocławia",
lat: 50.9044,
lng: 15.7197,
heroImage: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=1600&q=80",
seoTitle: "Klimatyzacja Jelenia Góra — Montaż i Serwis | ThermCool",
seoDescription: "Montaż klimatyzacji w Jeleniej Górze i Kotlinie Jeleniogórskiej. Certyfikat F-Gaz, bezpłatna wycena, serwis split i multisplit. ThermCool Dolny Śląsk.",
heroSubtitle: "Klimatyzacja w Jeleniej Górze — montaż, dobór i serwis urządzeń split. Obsługujemy całą Kotlinę Jeleniogórską.",
introText: "Jelenia Góra to brama w Karkonosze i jeden z ważniejszych ośrodków miejskich południowego Dolnego Śląska. Mimo górskiego klimatu latem temperatury w mieście są coraz wyższe, a klimatyzacja staje się standardem zarówno w hotelach i pensjonatach jak i w prywatnych domach. Obsługujemy Jelenią Górę oraz okolice — Kowary, Karpacz, Szklarską Porębę, Piechowice.",
localFacts: "W rejonie Jeleniej Góry realizujemy montaże z uwzględnieniem specyfiki budynków turystycznych i hotelowych — dyskretne prowadzenie instalacji, jednostki o niskim poziomie hałasu, systemy multisplit dla wielu pokojów obsługiwane jedną jednostką zewnętrzną.",
acHighlight: "W obiektach noclegowych i pensjonatach w okolicach Jeleniej Góry szczególnie polecamy systemy multisplit — jedna jednostka zewnętrzna obsługuje kilka pokojów, minimalizując ingerencję w elewację budynku przy zachowaniu pełnego komfortu gości.",
heatPumpTeaser: "Pompa ciepła w Jeleniej Górze — region górski to idealne warunki dla nowoczesnych pomp ciepła, które pracują wydajnie nawet w niskich temperaturach. Realizujemy instalacje z pełną pomocą w uzyskaniu dofinansowania.",
},
{
slug: "lubin",
name: "Lubin",
nameDopelniacz: "Lubinie",
nameDopelniacz2: "Lubina",
region: "Dolny Śląsk",
population: "73 tys.",
distanceFromHQ: "65 km od Wrocławia",
lat: 51.4003,
lng: 16.2031,
heroImage: "https://images.unsplash.com/photo-1486325212027-8081e485255e?w=1600&q=80",
seoTitle: "Klimatyzacja Lubin — Montaż i Serwis | ThermCool",
seoDescription: "Klimatyzacja Lubin — montaż split i multisplit dla domów, biur i obiektów przemysłowych. F-Gaz, bezpłatna wycena. ThermCool Dolny Śląsk.",
heroSubtitle: "Montaż klimatyzacji w Lubinie — dla domu, biura i zakładu pracy. Szybki termin, certyfikat F-Gaz, gwarancja jakości.",
introText: "Lubin to dynamicznie rozwijające się miasto Zagłębia Miedziowego. Obsługujemy zarówno prywatne domy i mieszkania jak i biura, hale produkcyjne i obiekty KGHM. W tym regionie klimatyzacja to nie luksus — to konieczność w zakładach pracy i standard oczekiwany przez pracowników biurowych. Obsługujemy Lubin i okoliczne miejscowości — Polkowice, Rudną, Ścinawę.",
localFacts: "W Lubinie i Zagłębiu Miedziowym realizujemy zarówno instalacje w nowych domach deweloperskich jak i w zakładach przemysłowych wymagających specjalistycznych systemów chłodzenia. Nasze doświadczenie obejmuje obiekty o powierzchni od kilkudziesięciu do kilku tysięcy metrów kwadratowych.",
acHighlight: "Dla firm z okolic Lubina oferujemy systemy VRF/VRV — zaawansowane rozwiązania klimatyzacyjne dla dużych powierzchni biurowych i hal. Wyceniamy kompleksowo: urządzenia, montaż i serwis w jednym pakiecie.",
heatPumpTeaser: "Pompa ciepła w Lubinie — właściciele domów jednorodzinnych w rejonie Lubina coraz chętniej wymieniają kotły gazowe i węglowe na pompy ciepła. Pomagamy w całym procesie — od audytu przez montaż po dotację.",
},
{
slug: "glogow",
name: "Głogów",
nameDopelniacz: "Głogowie",
nameDopelniacz2: "Głogowa",
region: "Dolny Śląsk",
population: "63 tys.",
distanceFromHQ: "95 km od Wrocławia",
lat: 51.6644,
lng: 16.0828,
heroImage: "https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1600&q=80",
seoTitle: "Klimatyzacja Głogów — Montaż i Serwis | ThermCool",
seoDescription: "Montaż klimatyzacji w Głogowie — certyfikowani instalatorzy F-Gaz, split i multisplit, bezpłatna wycena. Obsługujemy Głogów i powiat głogowski.",
heroSubtitle: "Klimatyzacja Głogów — profesjonalny montaż i serwis. Obsługujemy miasto i powiat głogowski z pełną gwarancją jakości.",
introText: "Głogów to historyczne miasto nad Odrą, w którym zapotrzebowanie na klimatyzację rośnie rok do roku — szczególnie w kamienicach centrum i nowych osiedlach na obrzeżach. Obsługujemy Głogów oraz powiązane miejscowości — Żukowice, Jerzmanowę, Pęcław. Dojazd z Wrocławia zajmuje nam około 1,5 godziny, planujemy trasy tak by w okolicach Głogowa realizować kilka zleceń w jednym wyjeździe.",
localFacts: "W Głogowie pracujemy głównie w zabudowie wielorodzinnej — bloki i kamienice wymagają precyzyjnego prowadzenia instalacji, często przez klatki schodowe lub piony techniczne. Mamy doświadczenie w tego typu realizacjach i zawsze uzgadniamy trasę z administratorem budynku.",
acHighlight: "Dla mieszkańców bloków w Głogowie polecamy klimatyzatory z cichą pracą jednostki zewnętrznej — minimalizuje to ewentualne zastrzeżenia sąsiadów i zarządców. Pracujemy z modelami o poziomie hałasu poniżej 40 dB.",
heatPumpTeaser: "Pompa ciepła w Głogowie i powiecie głogowskim — pomagamy właścicielom domów jednorodzinnych przejść z ogrzewania węglowego na ekologiczne i tańsze w eksploatacji pompy ciepła powietrze-woda.",
},
{
slug: "swidnica",
name: "Świdnica",
nameDopelniacz: "Świdnicy",
nameDopelniacz2: "Świdnicy",
region: "Dolny Śląsk",
population: "55 tys.",
distanceFromHQ: "55 km od Wrocławia",
lat: 50.8444,
lng: 16.4875,
heroImage: "https://images.unsplash.com/photo-1480714378408-67cf0d13bc1b?w=1600&q=80",
seoTitle: "Klimatyzacja Świdnica — Montaż i Serwis | ThermCool",
seoDescription: "Klimatyzacja Świdnica — montaż split i multisplit, serwis i przeglądy. Certyfikat F-Gaz, bezpłatna wycena. ThermCool obsługuje Świdnicę i okolice.",
heroSubtitle: "Montaż klimatyzacji w Świdnicy — dla domów, mieszkań i firm. Szybki termin, estetyczny montaż, pełna gwarancja.",
introText: "Świdnica to ważne centrum handlowe i przemysłowe subregionu wałbrzyskiego. Obsługujemy miasto i okoliczne gminy — Żarów, Strzegom, Dobromierz. Klimatyzacja w Świdnicy cieszy się rosnącą popularnością zarówno w sektorze mieszkaniowym jak i w licznych tutejszych zakładach produkcyjnych i centrach logistycznych.",
localFacts: "W okolicach Świdnicy realizujemy instalacje zarówno w zabudowie wielorodzinnej centrum jak i w nowych domach jednorodzinnych na osiedlach podmiejskich. Każde zlecenie poprzedza bezpłatna wizja lokalna i kosztorys.",
acHighlight: "Dla właścicieli domów jednorodzinnych w okolicach Świdnicy szczególnie polecamy klimatyzatory z funkcją grzania — zapewniają komfort przez cały rok i skutecznie ogrzewają nawet przy temperaturach zewnętrznych sięgających -20°C.",
heatPumpTeaser: "Pompa ciepła w Świdnicy i powiecie świdnickim — realizujemy instalacje powietrze-woda z pełnym wsparciem w procesie aplikacji o dofinansowanie z programu Czyste Powietrze.",
},
{
slug: "dzierzoniow",
name: "Dzierżoniów",
nameDopelniacz: "Dzierżoniowie",
nameDopelniacz2: "Dzierżoniowa",
region: "Dolny Śląsk",
population: "31 tys.",
distanceFromHQ: "60 km od Wrocławia",
lat: 50.7283,
lng: 16.6508,
heroImage: "https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=1600&q=80",
seoTitle: "Klimatyzacja Dzierżoniów — Montaż i Serwis | ThermCool",
seoDescription: "Klimatyzacja Dzierżoniów — profesjonalny montaż split i multisplit. Certyfikat F-Gaz, bezpłatna wycena. Obsługujemy Dzierżoniów i powiat dzierżoniowski.",
heroSubtitle: "Montaż klimatyzacji w Dzierżoniowie — split, multisplit, serwis. Certyfikowani monterzy, szybki termin realizacji.",
introText: "Dzierżoniów to miasto w powiecie dzierżoniowskim, w którym klimatyzacja zyskuje coraz więcej zwolenników — zarówno wśród właścicieli mieszkań jak i lokalnych przedsiębiorców. Obsługujemy Dzierżoniów oraz okoliczne miejscowości — Pieszyce, Niemcza, Łagiewniki. Nasi monterzy regularnie działają w tym rejonie, zapewniając krótkie terminy realizacji.",
localFacts: "W Dzierżoniowie realizujemy montaże głównie w budynkach wielorodzinnych i domach jednorodzinnych. Lokalne budownictwo z lat 70-90 wymaga niekiedy niestandardowych rozwiązań w zakresie prowadzenia instalacji — mamy doświadczenie w takich realizacjach.",
acHighlight: "Oferujemy klimatyzatory split w klasie energetycznej A++ i A+++ — niskie zużycie prądu to ważny argument dla mieszkańców Dzierżoniowa, gdzie koszty energii są istotnym elementem domowego budżetu.",
heatPumpTeaser: "Pompa ciepła w Dzierżoniowie — dobieramy urządzenia dostosowane do wielkości i izolacji budynku. Pomagamy w uzyskaniu dotacji z Czystego Powietrza, co znacząco obniża koszt inwestycji.",
},
{
slug: "bielawa",
name: "Bielawa",
nameDopelniacz: "Bielawie",
nameDopelniacz2: "Bielawy",
region: "Dolny Śląsk",
population: "29 tys.",
distanceFromHQ: "65 km od Wrocławia",
lat: 50.7167,
lng: 16.6167,
heroImage: "https://images.unsplash.com/photo-1444723121867-7a241cacace9?w=1600&q=80",
seoTitle: "Klimatyzacja Bielawa — Montaż i Serwis | ThermCool",
seoDescription: "Montaż klimatyzacji w Bielawie — split i multisplit, serwis i przeglądy. Uprawnienia F-Gaz, bezpłatna wycena. ThermCool Dolny Śląsk.",
heroSubtitle: "Klimatyzacja Bielawa — montaż i serwis klimatyzatorów split. Obsługujemy Bielawę i okolice z bezpłatnym audytem i jasną wyceną.",
introText: "Bielawa to miasto u podnóża Gór Sowich, gdzie mimo górskiego sąsiedztwa letnie upały dają się we znaki mieszkańcom. Obsługujemy Bielawę i okoliczne miejscowości — Pieszyce, Nowa Ruda. Klimatyzacja w Bielawie to coraz popularniejszy wybór właścicieli mieszkań w blokach z wielkiej płyty jak i nowych domów na obrzeżach.",
localFacts: "W Bielawie pracujemy przede wszystkim w budownictwie wielorodzinnym charakterystycznym dla miast poprzemysłowych Dolnego Śląska. Znamy specyfikę tych budynków i potrafimy sprawnie przeprowadzić montaż minimalizując niedogodności dla mieszkańców.",
acHighlight: "Dla mieszkańców bloków w Bielawie proponujemy rozwiązania z ukrytymi korytkami instalacyjnymi — estetyczne prowadzenie przewodów wewnątrz mieszkania i dyskretna jednostka zewnętrzna na balkonie lub elewacji.",
heatPumpTeaser: "Pompa ciepła w Bielawie — dla właścicieli domów jednorodzinnych to szansa na uniezależnienie się od rosnących cen gazu i węgla. Realizujemy instalacje z pomocą w dotacjach z Czystego Powietrza.",
},
{
slug: "olesnica",
name: "Oleśnica",
nameDopelniacz: "Oleśnicy",
nameDopelniacz2: "Oleśnicy",
region: "Dolny Śląsk",
population: "37 tys.",
distanceFromHQ: "30 km od Wrocławia",
lat: 51.2108,
lng: 17.3878,
heroImage: "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=1600&q=80",
seoTitle: "Klimatyzacja Oleśnica — Montaż i Serwis | ThermCool",
seoDescription: "Klimatyzacja Oleśnica — montaż split i multisplit blisko Wrocławia. Certyfikat F-Gaz, szybki termin, bezpłatna wycena. ThermCool Dolny Śląsk.",
heroSubtitle: "Montaż klimatyzacji w Oleśnicy — 30 km od Wrocławia, szybki dojazd, certyfikowani monterzy F-Gaz.",
introText: "Oleśnica leży zaledwie 30 km od Wrocławia, co czyni ją jednym z najbliżej obsługiwanych przez nas miast. Dojazd zajmuje niecałe 40 minut, dzięki czemu możemy zaoferować wyjątkowo krótkie terminy realizacji. Obsługujemy Oleśnicę i okoliczne miejscowości — Syców, Twardogóra, Dobroszyce.",
localFacts: "Oleśnica to miasto z dynamicznie rozwijającym się rynkiem nieruchomości — wiele osób z Wrocławia osiedla się tutaj ze względu na niższe ceny mieszkań przy dobrej komunikacji z miastem. Ta grupa klientów często wybiera klimatyzację jako standard wyposażenia nowego domu.",
acHighlight: "Dla nowych domów jednorodzinnych w Oleśnicy i okolicach polecamy kompleksowe systemy klimatyzacji przygotowane na etapie budowy — ukryte kanały, minimalna ingerencja w ściany, eleganckie rozwiązania dopasowane do projektu wnętrza.",
heatPumpTeaser: "Pompa ciepła w Oleśnicy — bliskość Wrocławia i dobra dostępność naszego serwisu sprawiają że obsługa instalacji jest wyjątkowo komfortowa. Realizujemy instalacje powietrze-woda z pełnym pakietem dofinansowania.",
},
{
slug: "olawa",
name: "Oława",
nameDopelniacz: "Oławie",
nameDopelniacz2: "Oławy",
region: "Dolny Śląsk",
population: "33 tys.",
distanceFromHQ: "25 km od Wrocławia",
lat: 50.9500,
lng: 17.2833,
heroImage: "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=1600&q=80",
seoTitle: "Klimatyzacja Oława — Montaż i Serwis | ThermCool",
seoDescription: "Klimatyzacja Oława — montaż split i multisplit 25 km od Wrocławia. Certyfikat F-Gaz, bezpłatna wycena, szybki dojazd. ThermCool Dolny Śląsk.",
heroSubtitle: "Montaż klimatyzacji w Oławie — 25 km od Wrocławia, najkrótsze terminy realizacji na Dolnym Śląsku.",
introText: "Oława to jedno z najbliższych nam miast — 25 km od siedziby firmy i zaledwie 25 minut drogi. To przekłada się na najkrótsze możliwe terminy realizacji w tym rejonie. Obsługujemy Oławę oraz okoliczne miejscowości — Jelcz-Laskowice, Domaniów, Skarbimierz. Klimatyzacja w Oławie cieszy się rosnącą popularnością wśród mieszkańców nowych osiedli i starszej zabudowy.",
localFacts: "W Oławie i Jelczu-Laskowicach obsługujemy zarówno nowe osiedla deweloperskie jak i starszą zabudowę. Bliskie położenie od naszej bazy sprawia że serwis gwarancyjny i pogwarancyjny realizujemy wyjątkowo sprawnie.",
acHighlight: "Mieszkańcy Oławy i okolic korzystają z naszej szybkiej reakcji serwisowej — w przypadku usterki nasi technicy są w stanie dotrzeć na miejsce w ciągu kilku godzin od zgłoszenia.",
heatPumpTeaser: "Pompa ciepła w Oławie — realizujemy instalacje dla domów jednorodzinnych w Oławie i całym powiecie oławskim. Krótki dojazd to też sprawny serwis przez cały okres użytkowania urządzenia.",
},
{
slug: "strzelin",
name: "Strzelin",
nameDopelniacz: "Strzelinie",
nameDopelniacz2: "Strzelina",
region: "Dolny Śląsk",
population: "13 tys.",
distanceFromHQ: "45 km od Wrocławia",
lat: 50.7833,
lng: 17.0667,
heroImage: "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=1600&q=80",
seoTitle: "Klimatyzacja Strzelin — Montaż i Serwis | ThermCool",
seoDescription: "Klimatyzacja Strzelin — montaż split i multisplit, serwis. Certyfikat F-Gaz, bezpłatna wycena. Obsługujemy Strzelin i powiat strzeliński.",
heroSubtitle: "Klimatyzacja Strzelin — montaż i serwis klimatyzatorów split. Obsługujemy Strzelin i okolice powiatu strzelinskiego.",
introText: "Strzelin to miasto znane z kamieniołomów granitu, położone ok. 45 km na południe od Wrocławia. Obsługujemy Strzelin i okoliczne miejscowości — Wiązów, Przeworno, Borów. Klimatyzacja w tym rejonie cieszy się rosnącym zainteresowaniem szczególnie wśród właścicieli domów jednorodzinnych.",
localFacts: "W Strzelinie i okolicach obsługujemy głównie zabudowę jednorodzinną — domy murowane i drewniane, nowe i remontowane. Znamy specyfikę lokalnego budownictwa i potrafimy dobrać optymalne rozwiązania dla każdego typu budynku.",
acHighlight: "Dla domów jednorodzinnych w powiecie strzelinskim polecamy klimatyzatory o podwyższonej odporności na trudne warunki zewnętrzne — jednostki zewnętrzne pracujące w trybie grzewczym nawet przy -20°C.",
heatPumpTeaser: "Pompa ciepła w Strzelinie — powiat strzeliński to rejon gdzie wiele domów nadal ogrzewa się węglem. Pompa ciepła to ekologiczna i ekonomiczna alternatywa, a dofinansowanie z Czystego Powietrza czyni ją realnie dostępną.",
},
{
slug: "sroda-slaska",
name: "Środa Śląska",
nameDopelniacz: "Środzie Śląskiej",
nameDopelniacz2: "Środy Śląskiej",
region: "Dolny Śląsk",
population: "9 tys.",
distanceFromHQ: "35 km od Wrocławia",
lat: 51.1667,
lng: 16.5833,
heroImage: "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=1600&q=80",
seoTitle: "Klimatyzacja Środa Śląska — Montaż i Serwis | ThermCool",
seoDescription: "Klimatyzacja Środa Śląska — montaż split i multisplit 35 km od Wrocławia. F-Gaz, bezpłatna wycena. ThermCool obsługuje Środę Śląską i okolice.",
heroSubtitle: "Montaż klimatyzacji w Środzie Śląskiej — blisko Wrocławia, szybki dojazd i termin realizacji.",
introText: "Środa Śląska to prężnie rozwijające się miasto powiatu średzkiego, 35 km od Wrocławia. Obsługujemy Środę Śląską i okoliczne gminy — Malczyce, Miękinia, Kostomłoty. Duża liczba nowych inwestycji mieszkaniowych w tej okolicy przekłada się na rosnące zapotrzebowanie na klimatyzację.",
localFacts: "Środa Śląska przyciąga nowych mieszkańców szukających spokojniejszego otoczenia przy dobrej komunikacji z Wrocławiem. Nowe domy w tej okolicy coraz częściej są wyposażane w klimatyzację już na etapie wykończenia.",
acHighlight: "Dla nowych domów w Środzie Śląskiej i okolicach oferujemy kompletne projekty instalacji klimatyzacji — od audytu i doboru mocy po finalny montaż i uruchomienie z szkoleniem z obsługi.",
heatPumpTeaser: "Pompa ciepła w Środzie Śląskiej — przy nowych domach jednorodzinnych w tej okolicy pompa ciepła jest często wybierana jako podstawowe źródło ogrzewania. Pomagamy w całym procesie inwestycji.",
},
{
slug: "trzebnica",
name: "Trzebnica",
nameDopelniacz: "Trzebnicy",
nameDopelniacz2: "Trzebnicy",
region: "Dolny Śląsk",
population: "13 tys.",
distanceFromHQ: "25 km od Wrocławia",
lat: 51.3108,
lng: 17.0611,
heroImage: "https://images.unsplash.com/photo-1482192505345-5852e5f0e520?w=1600&q=80",
seoTitle: "Klimatyzacja Trzebnica — Montaż i Serwis | ThermCool",
seoDescription: "Klimatyzacja Trzebnica — montaż split i multisplit 25 km od Wrocławia. Certyfikat F-Gaz, szybki termin, bezpłatna wycena. ThermCool Dolny Śląsk.",
heroSubtitle: "Montaż klimatyzacji w Trzebnicy — 25 km od Wrocławia, szybki dojazd, certyfikowani monterzy.",
introText: "Trzebnica to urokliwe miasto 25 km na północ od Wrocławia, znane z sanktuarium św. Jadwigi Śląskiej. Obsługujemy Trzebnicę i okoliczne gminy — Zawonia, Oborniki Śląskie, Wisznia Mała. Klimatyzacja w Trzebnicy jest coraz bardziej poszukiwana zarówno przez prywatnych mieszkańców jak i przez liczne tutejsze ośrodki zdrowotne i rehabilitacyjne.",
localFacts: "W Trzebnicy realizujemy instalacje w obiektach prywatnych i komercyjnych. Bliskość Wrocławia i dobra dostępność naszego serwisu sprawiają że obsługa posprzedażowa jest wyjątkowo sprawna.",
acHighlight: "Dla ośrodków zdrowotnych i rehabilitacyjnych w Trzebnicy oferujemy klimatyzatory z zaawansowaną filtracją powietrza — jonizatory, filtry PM2.5, funkcja sterylizacji UV — ważne w środowisku medycznym i uzdrowiskowym.",
heatPumpTeaser: "Pompa ciepła w Trzebnicy — obsługujemy zarówno prywatne domy jak i większe obiekty. Bliskość Wrocławia to szybki serwis i krótkie czasy reakcji przez cały okres użytkowania pompy.",
},
{
slug: "siechnice",
name: "Siechnice",
nameDopelniacz: "Siechnicach",
nameDopelniacz2: "Siechnic",
region: "Dolny Śląsk",
population: "12 tys.",
distanceFromHQ: "15 km od Wrocławia",
lat: 51.0500,
lng: 17.1333,
heroImage: "https://images.unsplash.com/photo-1502082553048-f009c37129b9?w=1600&q=80",
seoTitle: "Klimatyzacja Siechnice — Montaż i Serwis | ThermCool",
seoDescription: "Klimatyzacja Siechnice — montaż split i multisplit 15 km od Wrocławia. Najkrótszy dojazd, F-Gaz, bezpłatna wycena. ThermCool Dolny Śląsk.",
heroSubtitle: "Klimatyzacja Siechnice — 15 km od Wrocławia. Najkrótszy dojazd, ekspresowy termin realizacji.",
introText: "Siechnice to jedna z najszybciej rozwijających się gmin powiatu wrocławskiego — zaledwie 15 km od centrum Wrocławia. Ogromna liczba nowych inwestycji mieszkaniowych w tej gminie sprawia że jesteśmy tu bardzo aktywni. Obsługujemy Siechnice, Świętą Katarzynę, Radwanice i inne miejscowości gminy Siechnice.",
localFacts: "Siechnice to raj dla deweloperów i nowych mieszkańców Wrocławia. Nowe domy jednorodzinne i szeregówki to nasz główny rynek w tej gminie. Klimatyzacja jest tu często traktowana jako standard i planowana już na etapie projektu budynku.",
acHighlight: "Dla nowych domów w Siechnicach oferujemy instalacje przygotowywane na etapie stanu surowego — okablowanie poprowadzone w ścianach, otwory na jednostki zewnętrzne z gotowymi przepustami, minimalna ingerencja przy właściwym montażu.",
heatPumpTeaser: "Pompa ciepła w Siechnicach — przy nowych domach w tej gminie pompa ciepła jest standardowym wyborem. Realizujemy instalacje powietrze-woda i gruntowe, doradzamy w wyborze optymalnego rozwiązania dla danej działki.",
},
];
export const getCityBySlug = (slug: string): CityData | undefined =>
dolnySlaskCities.find((c) => c.slug === slug);
export const getAllCitySlugs = () =>
dolnySlaskCities.map((c) => ({ slug: c.slug }));
+1 -30
View File
@@ -2,7 +2,7 @@ export interface FaqItem {
id: string;
question: string;
answer: string;
category: 'klimatyzacja' | 'pompy-ciepla' | 'remonty' | 'hydraulika' | 'serwis-agd';
category: 'klimatyzacja' | 'pompy-ciepla';
}
export const faqData: FaqItem[] = [
@@ -70,33 +70,4 @@ export const faqData: FaqItem[] = [
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',
},
];
+658 -53
View File
@@ -1,69 +1,674 @@
import { Post } from "@/types/types";
export interface Post {
slug: string;
title: string;
category: 'klimatyzacja' | 'pompy-ciepla' | 'porady';
excerpt: string;
date: string;
author: string;
image: string;
content: string;
featured?: boolean;
}
// 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>`,
slug: 'jaki-klimatyzator-wybrac-do-mieszkania',
title: 'Jaki klimatyzator wybrać do mieszkania? Kompletny poradnik 2026',
category: 'klimatyzacja',
excerpt: 'Zastanawiasz się jaki klimatyzator wybrać do swojego mieszkania? Sprawdź nasz poradnik i poznaj różnice między systemami split i multisplit.',
date: '2026-01-05T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
featured: true,
content: `
<h2>Wybór odpowiedniego klimatyzatora to kluczowa decyzja</h2>
<p>Jaki klimatyzator wybrać do mieszkania, by zapewnić sobie komfort przez całe lato? Na to pytanie odpowiadamy najczęściej. Podstawową kwestią jest dobór mocy chłodniczej. Przyjmuje się, że na każde 10 m² pomieszczenia potrzebujemy około 1 kW mocy.</p>
<h3>Systemy Split i Multisplit</h3>
<p>System <strong>Split</strong> składa się z jednej jednostki wewnętrznej i jednej zewnętrznej. To najczęstszy wybór do chłodzenia pojedynczego salonu. Jeśli jednak chcesz schłodzić sypialnię, salon i pokój dziecka, idealnym rozwiązaniem będzie <strong>Multisplit</strong>, który pozwala podłączyć kilka jednostek wewnętrznych do jednego agregatu na balkonie.</p>
<h3>Na co zwrócić uwagę?</h3>
<ul>
<li><strong>Klasa energetyczna:</strong> Wybieraj modele A++ lub A+++, by zminimalizować rachunki za prąd.</li>
<li><strong>Głośność:</strong> W sypialni szukaj urządzeń o głośności poniżej 20 dB.</li>
<li><strong>Marka:</strong> Polecamy sprawdzonych producentów jak Gree, Rotenso czy Haier.</li>
</ul>
<p>Jeśli nadal nie wiesz, jaki model sprawdzi się najlepiej w Twoim mieszkaniu, skontaktuj się z nami. Zapewniamy bezpłatny audyt i dobór odpowiedniego sprzętu!</p>
`
},
{
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: 'ile-kosztuje-montaz-klimatyzacji',
title: 'Ile kosztuje montaż klimatyzacji w 2026 roku? Cennik i co wpływa na cenę',
category: 'klimatyzacja',
excerpt: 'Dowiedz się, ile kosztuje montaż klimatyzacji w 2026 roku. Sprawdź cennik usług, ukryte koszty i dowiedz się, jak mądrze porównywać oferty.',
date: '2026-01-12T10:00:00Z',
author: 'Janusz Nowak',
image: '',
content: `
<h2>Koszty montażu klimatyzacji - co musisz wiedzieć?</h2>
<p>Ile kosztuje montaż klimatyzacji w 2026 roku? To zależy od wielu czynników. Sam zakup urządzenia to tylko część inwestycji. Standardowy montaż systemu typu split kosztuje zazwyczaj od 1200 zł do 2000 zł, natomiast przy bardziej skomplikowanych systemach multisplit cena rośnie i wynosi od 2500 zł do nawet 5000 zł.</p>
<h3>Co obejmuje podstawowy montaż?</h3>
<ul>
<li>Wykonanie przewiertu przez ścianę.</li>
<li>Prowadzenie instalacji miedzianej w korytkach (do 3-5 metrów).</li>
<li>Podłączenie urządzenia do istniejącej sieci elektrycznej.</li>
<li>Sprawdzenie szczelności i uruchomienie systemu.</li>
</ul>
<h3>Ukryte koszty, o których warto pamiętać</h3>
<p>Często zdarza się, że instalacja wymaga dłuższego prowadzenia rur, kucia bruzd w ścianach czy użycia podnośnika koszowego, jeśli mieszkasz na wyższym piętrze. Należy także pamiętać, że instalacja w budownictwie mieszkaniowym objęta jest stawką VAT 8%, a w lokalach usługowych 23%.</p>
<p>Chcesz poznać dokładną wycenę dla Twojego domu? Zadzwoń do nas, przygotujemy ofertę bez ukrytych kosztów.</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: 'klimatyzacja-jako-ogrzewanie-zimowe',
title: 'Klimatyzacja jako ogrzewanie zimowe — czy to się opłaca?',
category: 'klimatyzacja',
excerpt: 'Zastanawiasz się nad wykorzystaniem klimatyzacji do ogrzewania? Odkryj zalety pompy ciepła powietrze-powietrze i dowiedz się, czy to opłacalne.',
date: '2026-01-18T10:00:00Z',
author: 'Marta Wiśniewska',
image: '',
content: `
<h2>Czy klimatyzator sprawdzi się zimą?</h2>
<p>Klimatyzacja jako ogrzewanie zimowe staje się w Polsce coraz bardziej popularna. Wynika to z faktu, że nowoczesne klimatyzatory to tak naprawdę pompy ciepła typu powietrze-powietrze. Działają one na zasadzie odwróconego obiegu chłodniczego, pobierając darmowe ciepło z powietrza na zewnątrz i przekazując je do wnętrza.</p>
<h3>Wydajność (COP) i koszty</h3>
<p>Opłacalność tego rozwiązania zależy głównie od współczynnika COP (Coefficient of Performance). Dobrej klasy urządzenia utrzymują wysoką sprawność nawet przy mrozach sięgających -20°C czy -25°C. Oznacza to, że z 1 kW pobranego prądu możemy uzyskać nawet 3-4 kW ciepła.</p>
<h3>Alternatywa dla gazu i prądu</h3>
<ul>
<li>Ogrzewanie prądem (farelki) – stosunek 1:1, bardzo drogie w eksploatacji.</li>
<li>Klimatyzacja (pompa ciepła) – stosunek 1:4, znacznie obniża rachunki za ogrzewanie okresowe.</li>
</ul>
<p>Jeśli szukasz oszczędności w okresie przejściowym (jesień, wczesna wiosna), klimatyzator z funkcją grzania będzie strzałem w dziesiątkę. Odwiedź nasz sklep, aby sprawdzić modele z najlepszymi parametrami grzewczymi.</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>`,
slug: 'pompa-ciepla-vs-gaz-co-wybrac',
title: 'Pompa ciepła vs gaz — co wybrać do ogrzewania domu?',
category: 'pompy-ciepla',
excerpt: 'Wybór między pompą ciepła a gazem to dylemat wielu inwestorów. Porównujemy koszty instalacji, eksploatacji i zwrot z inwestycji.',
date: '2026-01-25T10:00:00Z',
author: 'Janusz Nowak',
image: '',
featured: true,
content: `
<h2>Odwieczny dylemat: Pompa ciepła czy gaz?</h2>
<p>Pompa ciepła vs gaz to temat, który rozpala dyskusje na forach budowlanych. Obie technologie mają swoje zalety, ale wybór zależy przede wszystkim od standardu energetycznego Twojego budynku oraz dostępu do infrastruktury.</p>
<h3>Koszty inwestycyjne</h3>
<p>Kocioł gazowy to zazwyczaj koszt rzędu 8-15 tysięcy złotych, jednak wymaga podłączenia do sieci gazowej i komina, co generuje dodatkowe wydatki. Pompa ciepła jest droższa na starcie (30-60 tysięcy złotych), ale nie potrzebuje przyłącza gazowego ani kotłowni z wentylacją. Dzięki licznym dofinansowaniom, różnica w kosztach inwestycji szybko się zaciera.</p>
<h3>Koszty eksploatacji</h3>
<ul>
<li><strong>Sprawność:</strong> Kocioł gazowy ma sprawność około 95-98%. Pompa ciepła osiąga COP na poziomie 300-400%.</li>
<li><strong>Rachunki:</strong> W dobrze ocieplonym domu z ogrzewaniem podłogowym pompa ciepła będzie tańsza w utrzymaniu, zwłaszcza po połączeniu z fotowoltaiką.</li>
</ul>
<p>Pamiętaj jednak, że pompa ciepła nie sprawdzi się w nieocieplonych, starych domach z tradycyjnymi grzejnikami. Potrzebujesz rzetelnego audytu? Skontaktuj się z nami, a doradzimy najlepsze rozwiązanie dla Twojego domu!</p>
`
},
{
slug: 'dofinansowanie-czyste-powietrze-2026',
title: 'Czyste Powietrze 2026 — ile możesz dostać na pompę ciepła?',
category: 'pompy-ciepla',
excerpt: 'Program Czyste Powietrze w 2026 roku oferuje rekordowe dopłaty. Dowiedz się, kto może skorzystać, jakie są progi dochodowe i jak złożyć wniosek.',
date: '2026-02-02T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
featured: true,
content: `
<h2>Rekordowe dopłaty w programie Czyste Powietrze 2026</h2>
<p>Program Czyste Powietrze na rok 2026 przeszedł kilka ważnych modyfikacji, które ułatwiają inwestycję w odnawialne źródła energii. Jeśli planujesz wymianę "kopciucha" na nowoczesną pompę ciepła, to idealny moment na działanie.</p>
<h3>Progi dofinansowania</h3>
<p>Kwota, jaką możesz otrzymać, zależy od Twoich dochodów:</p>
<ul>
<li><strong>Poziom podstawowy:</strong> dla osób z rocznym dochodem do 135 000 zł.</li>
<li><strong>Poziom podwyższony:</strong> wyższa dotacja dla osób z niższym dochodem na członka rodziny.</li>
<li><strong>Poziom najwyższy:</strong> nawet do 100% kosztów (do 135 000 zł) dla najuboższych.</li>
</ul>
<h3>Jak uniknąć błędów?</h3>
<p>Najczęstszym błędem jest zakup urządzenia, które nie figuruje na liście ZUM (Zielonych Urządzeń i Materiałów). Przed opłaceniem faktury zawsze sprawdzaj certyfikaty. Nasi specjaliści pomagają nie tylko w doborze i montażu urządzeń, ale również w kompleksowym przygotowaniu wniosków o dotację. Chcesz wiedzieć więcej? Odezwij się do nas!</p>
`
},
{
slug: 'jak-dobrac-moc-klimatyzatora',
title: 'Jak dobrać moc klimatyzatora? Wzory i kalkulator dla każdego pomieszczenia',
category: 'klimatyzacja',
excerpt: 'Nie wiesz jaką moc powinien mieć Twój klimatyzator? Poznaj wzory obliczeniowe, współczynniki korekcyjne i zasady profesjonalnego doboru.',
date: '2026-02-08T10:00:00Z',
author: 'Janusz Nowak',
image: '',
content: `
<h2>Moc chłodnicza a powierzchnia — podstawowa zasada</h2>
<p>Jak dobrać moc klimatyzatora? To jedno z najważniejszych pytań. Zbyt słaby klimatyzator będzie pracował bez przerwy (zużywając mnóstwo prądu), a zbyt mocny wychłodzi pokój za szybko, powodując dyskomfort i częste cykle włącz/wyłącz.</p>
<h3>Wzór 1 kW na 10 m²</h3>
<p>Dla standardowych pomieszczeń (wysokość ok. 2,6 m) o przeciętnym nasłonecznieniu stosuje się prosty przelicznik: 100 W na każdy metr kwadratowy. Zatem salon o powierzchni 25 m² potrzebuje urządzenia o mocy ok. 2,5 kW.</p>
<h3>Współczynniki korekcyjne</h3>
<p>Wzór ten należy zmodyfikować, jeśli:</p>
<ul>
<li>Pokój ma duże okna od strony południowej lub zachodniej (dodaj 20-30% mocy).</li>
<li>Znajduje się na poddaszu bez izolacji (dodaj nawet 50%).</li>
<li>W pomieszczeniu przebywa wiele osób lub działa sprzęt elektroniczny wydzielający ciepło.</li>
</ul>
<p>Aby mieć pewność, że system będzie działał optymalnie, warto zlecić to profesjonalistom. Zadzwoń do nas, a bezpłatnie wyliczymy zapotrzebowanie dla Twojego mieszkania!</p>
`
},
{
slug: 'klimatyzator-split-czy-multisplit',
title: 'Split czy multisplit — który system wybrać dla domu lub mieszkania?',
category: 'klimatyzacja',
excerpt: 'Split czy multisplit? Poznaj zalety i wady obu rozwiązań, porównaj koszty i zdecyduj, który system klimatyzacji będzie lepszy dla Twoich potrzeb.',
date: '2026-02-15T10:00:00Z',
author: 'Marta Wiśniewska',
image: '',
content: `
<h2>Klimatyzacja do kilku pomieszczeń — co sprawdzi się lepiej?</h2>
<p>Wybierając klimatyzację do więcej niż jednego pokoju, stajesz przed dylematem: klimatyzator split czy multisplit? Oba rozwiązania mają swoje wyraźne plusy i minusy.</p>
<h3>System Multi Split (1 agregat + kilka jednostek wew.)</h3>
<p>Zalety to przede wszystkim estetyka (tylko jedno pudło na balkonie) oraz często niższe rachunki przy jednoczesnej pracy kilku jednostek. Wadą jest wyższy koszt zakupu na start oraz to, że w przypadku awarii jednostki zewnętrznej, tracisz chłodzenie w całym domu.</p>
<h3>Wiele systemów Split (np. 3 agregaty + 3 jednostki wew.)</h3>
<ul>
<li><strong>Niezależność:</strong> Awaria jednego agregatu nie wpływa na pozostałe.</li>
<li><strong>Elastyczność:</strong> Możesz rozłożyć inwestycję w czasie, instalując kolejne jednostki co roku.</li>
<li><strong>Minusy:</strong> Zagracony balkon lub elewacja.</li>
</ul>
<p>Z reguły przy 2-3 pomieszczeniach klienci decydują się na multisplit ze względów wizualnych i wymogów wspólnot mieszkaniowych. Chcesz poznać ofertę na system multisplit? Sprawdź nasze pakiety z montażem!</p>
`
},
{
slug: 'serwis-klimatyzacji-co-i-kiedy',
title: 'Serwis klimatyzacji — co obejmuje przegląd i kiedy go zrobić?',
category: 'porady',
excerpt: 'Regularny serwis klimatyzacji to gwarancja zdrowia i sprawnego działania. Dowiedz się, z czego składa się przegląd i jak często go wykonywać.',
date: '2026-02-22T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
content: `
<h2>Dlaczego przeglądy klimatyzacji są konieczne?</h2>
<p>Regularny serwis klimatyzacji to nie tylko warunek utrzymania gwarancji producenta, ale przede wszystkim inwestycja w Twoje zdrowie. W wilgotnym środowisku wymiennika ciepła łatwo o rozwój grzybów, pleśni i bakterii.</p>
<h3>Co robi profesjonalny serwisant?</h3>
<p>Prawidłowy przegląd (wykonywany min. raz do roku, a najlepiej na wiosnę i na jesień) obejmuje:</p>
<ul>
<li>Mycie i dezynfekcję filtrów oraz parownika preparatami grzybobójczymi.</li>
<li>Czyszczenie jednostki zewnętrznej (usunięcie liści, kurzu, pyłków).</li>
<li>Sprawdzenie szczelności układu chłodniczego i ciśnienia czynnika.</li>
<li>Czyszczenie tacy i udrożnienie odpływu skroplin.</li>
</ul>
<h3>Co możesz zrobić sam?</h3>
<p>Pomiędzy przeglądami warto samodzielnie myć siatkowe filtry wstępne pod bieżącą wodą. Zajmuje to 5 minut, a drastycznie poprawia przepływ powietrza i zmniejsza hałas urządzenia. Potrzebujesz umówić przegląd przed upałami? Zadzwoń do nas z wyprzedzeniem!</p>
`
},
{
slug: 'klimatyzacja-a-jakos-powietrza',
title: 'Klimatyzacja a jakość powietrza w domu — fakty i mity',
category: 'porady',
excerpt: 'Czy klimatyzacja osusza powietrze i powoduje przeziębienia? Obalamy mity i sprawdzamy, jak nowoczesne klimatyzatory poprawiają jakość powietrza.',
date: '2026-03-01T10:00:00Z',
author: 'Marta Wiśniewska',
image: '',
content: `
<h2>Zdrowe powietrze w erze smogu</h2>
<p>Klimatyzacja a jakość powietrza to temat obrosły wieloma mitami. Wiele osób obawia się "klimatyzacyjnej anginy" czy wysuszonego powietrza. Tymczasem poprawnie używana klimatyzacja jest sprzymierzeńcem naszego zdrowia.</p>
<h3>Filtry i jonizatory</h3>
<p>Nowoczesne klimatyzatory wyposażone są w zaawansowane filtry elektrostatyczne, PM2.5, a nawet filtry z aktywnym węglem i powłokami antybakteryjnymi z jonami srebra. Dzięki temu wyłapują kurz, pyłki roślin i alergeny. Wbudowane jonizatory dodatkowo neutralizują zapachy i zarodniki pleśni w całym pomieszczeniu.</p>
<h3>Fakty i mity</h3>
<ul>
<li><strong>Mit:</strong> Klimatyzacja powoduje przeziębienia. <strong>Fakt:</strong> Przeziębienia powoduje zbyt niska temperatura nawiewu (różnica między zew. a wew. nie powinna przekraczać 6-7°C) i nawiew skierowany bezpośrednio na ciało.</li>
<li><strong>Fakt:</strong> Klimatyzacja osusza powietrze. W upalne, parne dni to pożądany efekt, jednak zimą podczas dogrzewania warto pamiętać o dodatkowym nawilżaczu.</li>
</ul>
<p>Wybierając urządzenie, warto zwrócić uwagę na zaawansowane funkcje filtracji. Znajdziesz je w naszej ofercie klimatyzatorów premium!</p>
`
},
{
slug: 'pompa-ciepla-powietrze-woda-jak-dziala',
title: 'Pompa ciepła powietrze-woda — jak działa i dla kogo jest odpowiednia?',
category: 'pompy-ciepla',
excerpt: 'Poznaj zasadę działania pompy ciepła powietrze-woda. Dowiedz się, jakie warunki musi spełniać dom, aby to ekologiczne źródło ogrzewania było efektywne.',
date: '2026-03-05T10:00:00Z',
author: 'Janusz Nowak',
image: '',
content: `
<h2>Ekologiczne serce nowoczesnego domu</h2>
<p>Zasada działania pompy ciepła powietrze-woda jest zaskakująco prosta i przypomina działanie lodówki, tylko w odwrotną stronę. Zamiast wytwarzać ciepło (jak piec), pompa transportuje je ze środowiska (z chłodnego powietrza na zewnątrz) do instalacji C.O. w budynku.</p>
<h3>Klucz do sukcesu: niska temperatura zasilania</h3>
<p>Pompy ciepła osiągają najwyższą sprawność, gdy różnica temperatur między źródłem dolnym (dwór) a górnym (grzejniki) jest najmniejsza. Dlatego urządzenia te idealnie współpracują z ogrzewaniem płaszczyznowym (podłogówką), które wymaga zasilania wodą o temperaturze zaledwie 30-35°C.</p>
<h3>Wymagania dla budynku</h3>
<ul>
<li>Bardzo dobra termoizolacja (ściany, dach, okna trójszybowe).</li>
<li>Miejsce na posadowienie jednostki zewnętrznej z dala od okien sypialni sąsiada.</li>
<li>W przypadku starych grzejników żeliwnych – konieczność zastosowania drogich pomp wysokotemperaturowych.</li>
</ul>
<p>Myślisz o termomodernizacji i montażu pompy ciepła? Nasi instalatorzy przygotują projekt i dobiorą optymalną moc dla Twojego domu.</p>
`
},
{
slug: 'klimatyzacja-w-biurze-co-warto-wiedziec',
title: 'Klimatyzacja w biurze — jak wybrać system dla małej i średniej firmy?',
category: 'klimatyzacja',
excerpt: 'Zapewnij komfort swoim pracownikom. Przegląd systemów klimatyzacji biurowej, od kasetonowych po kanałowe i VRF. Przepisy BHP a klimatyzacja.',
date: '2026-03-12T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
content: `
<h2>Komfort pracy to wyższa wydajność</h2>
<p>Klimatyzacja w biurze to w obecnych czasach standard, a nie luksus. Zgodnie z kodeksem pracy, jeśli temperatura w biurze przekracza 28°C, pracodawca musi zapewnić chłodne napoje, a w skrajnych przypadkach skrócić czas pracy. Dlatego instalacja chłodzenia to inwestycja w ciągłość biznesową.</p>
<h3>Najpopularniejsze systemy do biur</h3>
<ul>
<li><strong>Klimatyzatory kasetonowe:</strong> Montowane w sufitach podwieszanych. Równomiernie rozprowadzają powietrze w 4 kierunkach, idealne na open space.</li>
<li><strong>Systemy kanałowe:</strong> Ukryte w sufitach, nawiew przez dyskretne kratki. Najbardziej estetyczne i ciche rozwiązanie.</li>
<li><strong>Systemy VRF/VRV:</strong> Zaawansowane sieci z jednym potężnym agregatem na dachu i dziesiątkami jednostek wewnętrznych.</li>
</ul>
<p>Montaż klimatyzacji w działającym biurze często wymaga prac w weekendy, aby nie zakłócać pracy zespołu. Oferujemy elastyczne terminy realizacji dla B2B oraz atrakcyjny leasing operacyjny sprzętu. Zadzwoń po darmową wycenę dla Twojej firmy.</p>
`
},
{
slug: 'jak-oszczedzac-na-klimatyzacji',
title: 'Jak oszczędzać prąd na klimatyzacji? 10 sprawdzonych sposobów',
category: 'porady',
excerpt: 'Chłodny dom bez wysokich rachunków. Zastosuj te proste triki i sposoby na ekonomiczne używanie klimatyzatora w czasie największych upałów.',
date: '2026-03-20T10:00:00Z',
author: 'Marta Wiśniewska',
image: '',
content: `
<h2>Komfort nie musi być drogi</h2>
<p>Jak oszczędzać prąd używając klimatyzacji latem? Nowoczesne klimatyzatory inwerterowe są bardzo energooszczędne, jednak złe nawyki mogą podwoić rachunki za prąd. Oto najważniejsze zasady.</p>
<h3>Złota zasada 24 stopni</h3>
<p>Największym błędem jest ustawianie pilota na 18°C z nadzieją, że pokój schłodzi się szybciej. Klimatyzator będzie pracował na pełnej mocy bez przerwy. Optymalna temperatura to 24-26°C. Różnica między dworem a domem nie powinna wynosić więcej niż 6-7 stopni.</p>
<h3>Proste triki na co dzień:</h3>
<ul>
<li><strong>Zasłoń okna:</strong> Zewnętrzne rolety lub grube zasłony zatrzymają 70% promieniowania słonecznego.</li>
<li><strong>Nie wyłączaj na chwilę:</strong> Inwerter pobiera najmniej prądu podtrzymując temperaturę. Jeśli wychodzisz na godzinę, zostaw go włączonego.</li>
<li><strong>Czyste filtry:</strong> Zapchany filtr to mniejszy przepływ powietrza i większe obciążenie dla sprężarki.</li>
</ul>
<p>Jeżeli Twój sprzęt ma moduł Wi-Fi, używaj harmonogramów, by schłodzić mieszkanie tuż przed powrotem z pracy. Szukasz energooszczędnego sprzętu z Wi-Fi w standardzie? Zajrzyj do naszego sklepu!</p>
`
},
{
slug: 'czynnik-chlodniczy-r32-r410a',
title: 'Czynnik chłodniczy R32 vs R410A — czym się różnią i co wybrać?',
category: 'klimatyzacja',
excerpt: 'Zrozum różnice między czynnikami chłodniczymi R32 i R410A. Dlaczego R32 staje się standardem i jakie to ma znaczenie dla Twojego portfela i środowiska?',
date: '2026-03-25T10:00:00Z',
author: 'Janusz Nowak',
image: '',
content: `
<h2>Rewolucja w gazach chłodniczych</h2>
<p>Jeśli przeglądasz parametry klimatyzatorów, na pewno natknąłeś się na oznaczenie czynnika chłodniczego. Do niedawna królował R410A, jednak obecnie absolutnym standardem jest czynnik R32. Co to oznacza dla przeciętnego użytkownika?</p>
<h3>Dlaczego R32 jest lepszy?</h3>
<p>R32 jest czynnikiem nowej generacji, który został wymuszony przez zaostrzające się przepisy unijne (tzw. ustawa F-gazowa). Ma o blisko 70% niższy potencjał tworzenia efektu cieplarnianego (GWP) w porównaniu do R410A.</p>
<ul>
<li><strong>Większa wydajność:</strong> R32 lepiej przenosi ciepło, co przekłada się na wyższą klasę energetyczną urządzeń.</li>
<li><strong>Mniejsza ilość w układzie:</strong> Do napełnienia instalacji potrzeba nawet 30% mniej gazu.</li>
<li><strong>Tańszy serwis:</strong> Z uwagi na przepisy, R410A jest wycofywany, co drastycznie winduje jego cenę podczas ewentualnego dobijania czynnika.</li>
</ul>
<p>Kupując u nas, masz pewność, że wszystkie urządzenia pracują na nowoczesnym i ekologicznym czynniku R32 (lub w przypadku pomp ciepła, nadchodzącym propaniem R290). Stawiamy na przyszłościowe technologie!</p>
`
},
{
slug: 'montaz-klimatyzacji-w-bloku',
title: 'Montaż klimatyzacji w bloku — zgoda spółdzielni i praktyczne porady',
category: 'klimatyzacja',
excerpt: 'Planujesz klimatyzację w bloku? Dowiedz się jak uzyskać zgodę spółdzielni, gdzie powiesić agregat i uniknąć konfliktów z sąsiadami.',
date: '2026-04-02T10:00:00Z',
author: 'Marta Wiśniewska',
image: '',
content: `
<h2>Klimatyzacja w bloku to nie samowolka</h2>
<p>Upały w mieszkaniach w bloku bywają nieznośne, a klimatyzacja jest jedynym ratunkiem. Zanim jednak zamówisz montaż, musisz zadbać o formalności. W większości przypadków elewacja budynku oraz płyta balkonu stanowią część wspólną nieruchomości.</p>
<h3>Jak uzyskać zgodę?</h3>
<p>Wniosek do spółdzielni lub wspólnoty powinien zawierać rysunek pokazujący umiejscowienie agregatu oraz specyfikację urządzenia (w tym poziom hałasu i wymiary). Urzędy zazwyczaj zgadzają się bez problemu, pod warunkiem montażu na posadzce balkonu (na specjalnych nóżkach antywibracyjnych), a nie na elewacji, by nie psuć estetyki budynku.</p>
<h3>Odprowadzenie skroplin</h3>
<ul>
<li>Do pojemnika na balkonie (wymaga regularnego opróżniania).</li>
<li>Włączenie do pionu kanalizacyjnego (najlepsze rozwiązanie).</li>
<li>Na zewnątrz, poza obrys balkonu (często zakazane przez wspólnoty ze względu na kapanie na chodnik).</li>
</ul>
<p>Mamy ogromne doświadczenie w montażu klimatyzacji w domach wielorodzinnych na Dolnym Śląsku. Pomożemy Ci przygotować dokumentację dla spółdzielni!</p>
`
},
{
slug: 'klimatyzacja-dla-alergikow',
title: 'Klimatyzacja dla alergika — na jakie funkcje zwrócić uwagę?',
category: 'porady',
excerpt: 'Alergie na pyłki uciążliwe w domu? Odpowiednio dobrana klimatyzacja ze sterylizatorem i jonizacją może oczyścić powietrze z alergenów.',
date: '2026-04-08T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
content: `
<h2>Czyste powietrze w czasie pylenia</h2>
<p>Wiosna i lato to koszmar dla alergików. Otwieranie okien w celu przewietrzenia zaprasza pyłki do środka. Zamknięte okna i włączona klimatyzacja to świetne rozwiązanie, o ile wybierzesz model dedykowany osobom wrażliwym na alergeny.</p>
<h3>Technologie, których powinieneś szukać</h3>
<p>Podstawowe filtry siatkowe zatrzymają jedynie największy kurz. Alergicy powinni zwrócić uwagę na zaawansowane systemy oczyszczania:</p>
<ul>
<li><strong>Filtry plazmowe / elektrostatyczne:</strong> Wyłapują cząstki kurzu, pyłków drzew i traw ze skutecznością ponad 95%.</li>
<li><strong>Lampy UV-C:</strong> Sterylizują powietrze przepływające przez wymiennik, neutralizując wirusy i bakterie.</li>
<li><strong>Jonizatory (np. Nanoe-G, Flash Streamer):</strong> Unieszkodliwiają zanieczyszczenia osadzone na meblach i w dywanach.</li>
</ul>
<p>Dla alergików absolutnym priorytetem musi być regularny serwis. Bez czyszczenia klimatyzator sam stanie się siedliskiem roztoczy i pleśni. Zapytaj naszą obsługę o modele marek premium ze wsparciem dla zdrowego powietrza.</p>
`
},
{
slug: 'klimatyzacja-wroclaw-kiedy-montowac',
title: 'Klimatyzacja Wrocław — kiedy najlepiej zamówić montaż?',
category: 'klimatyzacja',
excerpt: 'Nie czekaj na lipcowe upały! Dowiedz się, dlaczego wiosna to najlepszy moment na zakup i montaż klimatyzacji we Wrocławiu i na Dolnym Śląsku.',
date: '2026-04-15T10:00:00Z',
author: 'Marta Wiśniewska',
image: '',
content: `
<h2>Klimatyzacja we Wrocławiu: Bądź szybszy niż słońce</h2>
<p>Klimatyzacja Wrocław to hasło, które bije rekordy popularności w wyszukiwarkach za każdym razem, gdy słupki rtęci na Dolnym Śląsku przekraczają 30 stopni. Niestety, szukanie wykonawcy w środku fali upałów to błąd, który kosztuje czas i pieniądze.</p>
<h3>Dlaczego wiosna to złoty czas na montaż?</h3>
<p>Sezon na klimatyzację zaczyna się w połowie maja, a apogeum osiąga w lipcu i sierpniu. Czekając do ostatniej chwili, musisz liczyć się z:</p>
<ul>
<li>Długimi terminami oczekiwania (często 4-6 tygodni).</li>
<li>Brakami sprzętowymi w hurtowniach (najtańsze i najlepsze modele wyprzedają się najszybciej).</li>
<li>Brakiem możliwości negocjacji cen — monterzy pracują od świtu do zmierzchu.</li>
</ul>
<p>Zamawiając instalację w marcu lub w kwietniu, masz pełen wybór urządzeń, czas na przemyślenie układu instalacji oraz szybki termin realizacji, często w przeciągu tygodnia. Chcesz założyć klimatyzację we Wrocławiu zanim zaczną się upały? Skontaktuj się z nami już dziś!</p>
`
},
{
slug: 'jak-dziala-inwenter-klimatyzator',
title: 'Jak działa klimatyzator inwerterowy i dlaczego warto go kupić?',
category: 'klimatyzacja',
excerpt: 'Technologia inwerterowa zrewolucjonizowała rynek HVAC. Tłumaczymy przystępnie, jak płynna modulacja sprężarki obniża rachunki za prąd.',
date: '2026-04-20T10:00:00Z',
author: 'Janusz Nowak',
image: '',
content: `
<h2>Koniec z szarpaniem energii</h2>
<p>Stare klimatyzatory (typu on/off) działały jak zwykła lodówka – włączały się na pełną moc (100%), chłodziły pomieszczenie z nadwyżką, wyłączały się całkowicie, a po podniesieniu temperatury proces zaczynał się od nowa. Powodowało to ogromne skoki napięcia, zużycie prądu i hałas. Dzisiejszy standard to klimatyzator inwerterowy.</p>
<h3>Na czym polega inwerter?</h3>
<p>Technologia inwerterowa pozwala na płynną regulację prędkości obrotowej sprężarki. Zamiast ciągłego włączania i wyłączania, sprężarka "zwalnia" pracę, utrzymując zadaną temperaturę z minimalnym zużyciem energii (nawet na poziomie 10-20% maksymalnej mocy).</p>
<ul>
<li><strong>Oszczędność:</strong> Do 40% mniejsze zużycie prądu.</li>
<li><strong>Żywotność:</strong> Mniej cykli rozruchu to wolniejsze zużycie komponentów mechanicznych.</li>
<li><strong>Komfort:</strong> Idealnie równomierna temperatura bez podmuchów lodowatego powietrza.</li>
</ul>
<p>Dzięki inwerterom, klimatyzacja jest obecnie bardzo tania w eksploatacji. Sprawdź naszą ofertę inwerterowych systemów z certyfikatem A+++.</p>
`
},
{
slug: 'pompy-ciepla-dolny-slask-dofinansowanie',
title: 'Pompy ciepła na Dolnym Śląsku — dofinansowanie i realizacje',
category: 'pompy-ciepla',
excerpt: 'Sprawdź, jak działają pompy ciepła na terenie Dolnego Śląska, jakie dofinansowania lokalne możesz uzyskać i jak dbamy o profesjonalny montaż.',
date: '2026-04-25T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
content: `
<h2>Dolny Śląsk to idealny region dla pomp ciepła</h2>
<p>Dzięki stosunkowo łagodnym zimom na nizinach Dolnego Śląska (Wrocław, Legnica, Lubin), powietrzne pompy ciepła osiągają u nas fenomenalne wyniki wydajnościowe (SCOP) przez cały sezon grzewczy. Nawet w rejonach przedgórskich (Wałbrzych, Jelenia Góra) nowoczesne instalacje świetnie radzą sobie bez wspomagania grzałkami.</p>
<h3>Wsparcie finansowe dla mieszkańców</h3>
<p>Poza ogólnopolskim programem "Czyste Powietrze", wiele dolnośląskich gmin i miast (m.in. Wrocław z programem KAWKA Plus) oferuje dodatkowe dotacje na likwidację pieców węglowych. Połączenie lokalnych funduszy z rządowymi potrafi sfinansować nawet do 80-90% całej inwestycji.</p>
<h3>Zaufaj lokalnym ekspertom</h3>
<ul>
<li>Znamy specyfikę dolnośląskiego budownictwa.</li>
<li>Zapewniamy szybki serwis gwarancyjny (nasza baza to okolice Wrocławia/Legnicy).</li>
<li>Pomagamy z biurokracją we wrocławskim i lokalnych urzędach.</li>
</ul>
<p>Chcesz dołączyć do grona ekologicznych domów w naszym województwie? Zostaw kontakt, a przeanalizujemy Twoje możliwości dotacyjne!</p>
`
},
{
slug: 'klimatyzacja-legnica-montaz',
title: 'Klimatyzacja Legnica — montaż split i multisplit w mieście i okolicach',
category: 'klimatyzacja',
excerpt: 'Montaż klimatyzacji w Legnicy z ThermCool. Doświadczenie w blokach, kamienicach i domkach. Szybkie terminy i darmowa wycena na miejscu.',
date: '2026-05-02T10:00:00Z',
author: 'Marta Wiśniewska',
image: '',
content: `
<h2>Chłodna ulga na wyciągnięcie ręki w Legnicy</h2>
<p>Lato w mieście potrafi dać się we znaki. Prowadząc działalność na Dolnym Śląsku, Legnica to dla nas kluczowy rynek, gdzie regularnie montujemy systemy split i multisplit dla klientów indywidualnych i biznesowych.</p>
<h3>Różne budynki, te same wyzwania</h3>
<p>Montaż klimatyzacji w Legnicy obejmuje zarówno wielką płytę na Piekarach, poniemieckie kamienice w Centrum, jak i nowe inwestycje deweloperskie. Każdy budynek wymaga innego podejścia:</p>
<ul>
<li><strong>Kamienice:</strong> Wymagane zgody konserwatora zabytków na montaż na elewacji. Często agregat ukrywamy na balkonach od strony podwórza.</li>
<li><strong>Nowe osiedla:</strong> Coraz częściej deweloperzy wydzielają specjalne miejsca na balkonie lub dachu na postawienie jednostki zewnętrznej.</li>
</ul>
<p>Dzięki stacjonarnym ekipom, czas dojazdu i reakcji na zgłoszenia serwisowe w Legnicy to zazwyczaj nie więcej niż 24-48h. Zapraszamy do rezerwacji bezpłatnych oględzin i audytu w Twoim mieszkaniu!</p>
`
},
{
slug: 'klimatyzacja-walbrzych-ceny',
title: 'Klimatyzacja Wałbrzych — ceny montażu i serwisu w 2026 roku',
category: 'klimatyzacja',
excerpt: 'Poznaj koszty i specyfikę instalacji klimatyzacji w Wałbrzychu. Górski klimat to doskonały powód, by używać klimatyzatora z funkcją grzania.',
date: '2026-05-08T10:00:00Z',
author: 'Janusz Nowak',
image: '',
content: `
<h2>Klimatyzacja nie tylko na lato</h2>
<p>Wałbrzych i jego górzyste okolice charakteryzują się nieco chłodniejszym klimatem i bardziej srogimi zimami niż reszta Dolnego Śląska. Dlatego klimatyzatory montowane w tym regionie niezwykle często służą jako efektywne źródło dogrzewania (pompy ciepła powietrze-powietrze) w sezonach przejściowych i zimą.</p>
<h3>Ile to kosztuje w 2026 roku?</h3>
<p>Ceny montażu w Wałbrzychu nie różnią się znacząco od ogólnopolskich. Na koszt końcowy składa się:</p>
<ul>
<li>Cena wybranego sprzętu (inwertery od ok. 2500 zł).</li>
<li>Standardowy montaż (ok. 1300-1500 zł).</li>
<li>Ewentualne prace dodatkowe, jak kucie trudnych konstrukcji betonowych czy przedłużanie instalacji freonowej.</li>
</ul>
<p>Nasza wycena zawsze jest darmowa i ostateczna. Po audycie w Wałbrzychu lub Świdnicy otrzymujesz gwarancję ceny — nie zaskoczymy Cię dodatkowymi kosztami ukrytymi na fakturze końcowej. Sprawdź nas!</p>
`
},
{
slug: 'klimatyzacja-jelenia-gora-pensjonat',
title: 'Klimatyzacja dla pensjonatów i hoteli w Jeleniej Górze i Karkonoszach',
category: 'klimatyzacja',
excerpt: 'Dobre opinie na Booking zależą od komfortu w upały. Specyfika montażu klimatyzacji dla branży HoReCa w Karkonoszach i Jeleniej Górze.',
date: '2026-05-12T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
content: `
<h2>Inwestycja w oceny na Booking.com</h2>
<p>Hotele i pensjonaty w Jeleniej Górze, Karpaczu czy Szklarskiej Porębie żyją z turystyki przez cały rok. O ile zimą kluczowe jest ogrzewanie, o tyle letnie upały w dusznych, poddaszowych pokojach prowadzą do lawiny słabych ocen od gości. Klimatyzacja to dziś konieczność.</p>
<h3>Systemy Multi Split dla obiektów turystycznych</h3>
<p>W hotelarstwie najlepiej sprawdzają się rozbudowane instalacje multisplit lub systemy VRF/VRV.</p>
<ul>
<li><strong>Estetyka elewacji:</strong> Kilkanaście pokoi obsługiwanych przez 1-2 agregaty schowane na płaskim dachu lub za budynkiem.</li>
<li><strong>Centralne sterowanie:</strong> Recepcja może zablokować widełki temperatur (np. min. 22°C), odciąć chłodzenie w pustych pokojach lub wyłączyć je po otwarciu okna (kontaktrony).</li>
<li><strong>Cisza absolutna:</strong> W pokojach hotelowych wymogiem jest tryb super silent poniżej 20 dB, aby nie zakłócać snu.</li>
</ul>
<p>Przygotowujemy zaawansowane projekty dla branży HoReCa w Karkonoszach z bezinwazyjnym montażem (ukrycie instalacji na strychach). Skontaktuj się z działem B2B ThermCool.</p>
`
},
{
slug: 'pompa-ciepla-stary-dom',
title: 'Pompa ciepła w starym domu — czy to możliwe bez termomodernizacji?',
category: 'pompy-ciepla',
excerpt: 'Masz dom z lat 70. lub 80.? Sprawdź, czy i na jakich warunkach instalacja pompy ciepła bez generalnego remontu elewacji ma sens.',
date: '2026-05-18T10:00:00Z',
author: 'Janusz Nowak',
image: '',
content: `
<h2>Stary dom, nowa technologia grzewcza</h2>
<p>Wielu inwestorów uważa, że montaż pompy ciepła powietrze-woda ma sens wyłącznie w nowoczesnym budownictwie z ogrzewaniem podłogowym. Pompa ciepła w starym domu (tzw. "wampirze energetycznym") budzi obawy o horrendalne rachunki za prąd. Prawda leży jednak pośrodku.</p>
<h3>Kiedy pompa ciepła to zły pomysł?</h3>
<p>Jeśli budynek nie posiada żadnego ocieplenia (goła cegła lub pustak), ma stare nieszczelne okna i dach z którego ucieka ciepło – pompa ciepła nie zdoła nagrzać go tanio, a urządzenie będzie musiało działać ciągle na maksymalnych obrotach (co szybko je zużyje).</p>
<h3>Rozwiązania dla domów modernizowanych</h3>
<ul>
<li><strong>Pompy wysokotemperaturowe:</strong> Nowoczesne jednostki potrafią dostarczyć wodę o temp. 65°C, by zasilić stare żeliwne grzejniki, przy wciąż zadowalającym COP.</li>
<li><strong>Układy hybrydowe (bivalent):</strong> Pompa ciepła współpracuje z istniejącym kotłem gazowym lub węglowym – pompa grzeje do temperatury ok. 0°C (gdzie jest najtańsza), a przy większych mrozach pracę przejmuje kocioł.</li>
</ul>
<p>Zawsze zaczynamy od darmowego audytu izolacyjności budynku. Jeśli konieczna jest wcześniejsza wymiana okien lub docieplenie dachu, szczerze Ci o tym powiemy.</p>
`
},
{
slug: 'klimatyzacja-nowe-mieszkanie-deweloper',
title: 'Klimatyzacja w nowym mieszkaniu od dewelopera — kiedy ją zainstalować?',
category: 'klimatyzacja',
excerpt: 'Odbierasz klucze od dewelopera? Najlepszy czas na planowanie klimatyzacji to etap stanu deweloperskiego. Sprawdź dlaczego to takie ważne.',
date: '2026-05-22T10:00:00Z',
author: 'Marta Wiśniewska',
image: '',
content: `
<h2>Wczesne planowanie = ukryte instalacje</h2>
<p>Odbiór mieszkania od dewelopera to idealny moment na przygotowanie infrastruktury pod klimatyzację. Błędem jest czekanie do upalnego lata, gdy ściany są już pomalowane, a podłogi ułożone.</p>
<h3>Montaż dwuetapowy</h3>
<p>Dla nowych mieszkań najpopularniejszym rozwiązaniem jest instalacja dwuetapowa:</p>
<ul>
<li><strong>Etap I (stan deweloperski / remont):</strong> Kucie bruzd w ścianach, ułożenie instalacji miedzianej, zasilania i odpływu skroplin w ścianie, zaszpachlowanie dziur. Całość ukryta pod tynkiem.</li>
<li><strong>Etap II (po pomalowaniu ścian):</strong> Powieszenie czystej jednostki wewnętrznej na gotowej ścianie oraz montaż agregatu na zewnątrz. System zostaje uruchomiony bez odrobiny kurzu.</li>
</ul>
<p>Dzięki takiej strategii unikasz prowadzenia brzydkich, białych korytek plastikowych po eleganckich ścianach salonu. Współpracujemy z ekipami wykończeniowymi, by idealnie wpasować się w harmonogram prac. Pamiętaj też, by na najwcześniejszym etapie zapytać administrację o zgodę!</p>
`
},
{
slug: 'pompa-ciepla-fotowoltaika-synergia',
title: 'Pompa ciepła + fotowoltaika — idealne połączenie dla samowystarczalności',
category: 'pompy-ciepla',
excerpt: 'Darmowe ogrzewanie i chłodzenie? Tak, jeśli połączysz pompę ciepła z instalacją fotowoltaiczną. Tłumaczymy jak działa autokonsumpcja i dobór mocy.',
date: '2026-05-27T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
featured: true,
content: `
<h2>Święty Graal energetyki domowej</h2>
<p>Połączenie pompy ciepła z fotowoltaiką (PV) to najpotężniejsze narzędzie w walce z rosnącymi cenami energii. Z jednej strony pompa ciepła do wygenerowania ciepła zużywa prąd, z drugiej — PV go bezpłatnie produkuje ze słońca.</p>
<h3>Autokonsumpcja i magazynowanie energii</h3>
<p>Problem polega na tym, że słońce najmocniej świeci latem, a prądu do ogrzewania potrzebujemy zimą. W aktualnym systemie (net-billing) prąd wyprodukowany latem odsprzedajemy do sieci po średniej cenie rynkowej, a zimą kupujemy po ustalonej stawce. Opłacalność wciąż jest ogromna, ale kluczem staje się zwiększanie autokonsumpcji.</p>
<ul>
<li>Zimą używamy PV do zasilania pompy ciepła w słoneczne poranki.</li>
<li>Latem nadmiar prądu napędza klimatyzatory schładzające dom (darmowe chłodzenie!).</li>
<li>Koszty domykamy instalując magazyn ciepła (bufor) lub bateryjny magazyn energii, by przechować darmowy prąd do wieczora.</li>
</ul>
<p>Program "Mój Prąd" mocno promuje instalacje hybrydowe (PV + Pompa + Magazyn). Chcesz maksymalnie obniżyć koszty życia w swoim domu na dekady? Wybierz inteligentny system zarządzania energią od ThermCool.</p>
`
},
{
slug: 'klimatyzacja-server-room',
title: 'Klimatyzacja serwerowni i szafy rack — wymagania i rozwiązania',
category: 'klimatyzacja',
excerpt: 'Bezpieczeństwo IT zależy od temperatury. Jak zaprojektować niezawodną klimatyzację precyzyjną do małej i średniej serwerowni.',
date: '2026-05-30T10:00:00Z',
author: 'Janusz Nowak',
image: '',
content: `
<h2>Chłodzenie IT nie uznaje kompromisów</h2>
<p>Zatrzymanie usług informatycznych na skutek przegrzania to dla firmy ogromne straty finansowe i wizerunkowe. Klimatyzacja serwerowni różni się drastycznie od klimatyzacji biurowej – musi pracować 24 godziny na dobę, 365 dni w roku, niezależnie od tego, czy za oknem jest +35°C, czy -25°C.</p>
<h3>Redundancja to absolutna podstawa</h3>
<p>W profesjonalnych projektach stosuje się systemy redundantne (tzw. praca naprzemienna 1+1 lub N+1). Polega to na zainstalowaniu dwóch niezależnych urządzeń współpracujących ze sobą przez specjalny sterownik (tzw. rotator). Jeśli jedno urządzenie ulegnie awarii, drugie natychmiast przejmuje pracę, zapobiegając usmażeniu sprzętu IT.</p>
<h3>Precyzyjna kontrola</h3>
<ul>
<li>Wymagany jest ciągły pomiar temperatury stref zimnych i ciepłych (idealnie 20-24°C).</li>
<li>Kontrola wilgotności – serwery nienawidzą przesuszonego powietrza (wyładowania elektrostatyczne), ani zbyt wilgotnego (zwarcia).</li>
<li>Praca całoroczna: jednostki zew. muszą być wyposażone w grzałki karteru i tacy ociekowej do zimowej pracy.</li>
</ul>
<p>Projektujemy i wykonujemy systemy chłodzenia IT gwarantujące 99,99% uptime. Zabezpiecz swoje dane z nami!</p>
`
},
{
slug: 'klimatyzacja-dom-jednorodzinny-system',
title: 'Klimatyzacja w domu jednorodzinnym — jaki system wybrać?',
category: 'klimatyzacja',
excerpt: 'Budujesz dom? Sprawdź najpopularniejsze konfiguracje klimatyzacji kanałowej, VRF i Multisplit, aby zapewnić chłód w każdym pomieszczeniu.',
date: '2026-06-03T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
content: `
<h2>Zintegrowane chłodzenie nowoczesnego domu</h2>
<p>Dom jednorodzinny daje największe pole do popisu przy projektowaniu systemów wentylacji i klimatyzacji. Montaż 4 oddzielnych klimatyzatorów Split z 4 agregatami psującymi elewację odchodzi do lamusa. Dziś stawia się na zintegrowane rozwiązania.</p>
<h3>Klimatyzatory kanałowe — królowie estetyki</h3>
<p>W domach parterowych i na poddaszach największym hitem jest system kanałowy. Jedna wydajna jednostka wewnętrzna ukryta na nieużytkowym strychu chłodzi powietrze, które rozprowadzane jest izolowanymi rurami do wszystkich pokoi. W suficie widoczne są tylko wąskie, designerskie szczeliny nawiewne, tzw. anemostaty liniowe.</p>
<h3>Multisplit do zadań specjalnych</h3>
<ul>
<li>Łączy zalety braku kanałów wentylacyjnych z jednym, wspólnym agregatem zewnętrznym na całą bryłę budynku.</li>
<li>Umożliwia niezależne sterowanie temperaturą w każdym pokoju (w sypialni 20°C, w salonie 24°C).</li>
<li>Integracja z aplikacjami mobilnymi do stworzenia spójnego Smart Home.</li>
</ul>
<p>Planowanie klimatyzacji kanałowej lub multisplit powinno rozpocząć się na etapie projektowania lub tuż przed wykańczaniem. Zadzwoń do nas po darmową wizję lokalną na budowie.</p>
`
},
{
slug: 'serwis-klimatyzacji-samemu-co-mozna',
title: 'Co w klimatyzacji możesz zrobić sam, a co musi serwisant z F-Gaz?',
category: 'porady',
excerpt: 'Chcesz oszczędzić na serwisie? Dowiedz się, które elementy klimatyzatora możesz wyczyścić bezpiecznie samodzielnie, a kiedy wezwać specjalistę.',
date: '2026-06-08T10:00:00Z',
author: 'Marta Wiśniewska',
image: '',
content: `
<h2>Samodzielny serwis – oszczędność czy ryzyko?</h2>
<p>Zlecanie kompleksowego serwisu zewnętrznej firmie raz do roku jest ważne, ale między przeglądami urządzenie szybko łapie kurz. Nie ze wszystkim musisz od razu dzwonić do firmy HVAC.</p>
<h3>Bezpieczne czynności dla użytkownika (DIY):</h3>
<ul>
<li><strong>Mycie filtrów wstępnych:</strong> Otwórz panel jednostki, wyjmij siateczki, umyj w ciepłej wodzie z mydłem, wysusz i włóż. Rób to raz w miesiącu!</li>
<li><strong>Przetarcie obudowy:</strong> Czystą ścierką z łagodnym detergentem.</li>
<li><strong>Oględziny jednostki na dworze:</strong> Usunięcie pajęczyn i liści z kratek, by nie tamowały przepływu powietrza.</li>
</ul>
<h3>Czego absolutnie NIE robić samodzielnie:</h3>
<p>Zgodnie z unijnymi przepisami o F-gazach, do ingerencji w układ chłodniczy dopuszczone są wyłącznie osoby z państwowym certyfikatem UDT. Podłączanie manometrów, próby szczelności czy nabijanie gazu grozi bardzo surowymi karami finansowymi, a niewprawnemu amatorowi – odmrożeniami dłoni (gaz rozpręża się do -40°C). Od tych zadań masz nas!</p>
`
},
{
slug: 'halas-klimatyzatora-jak-zmierzyc',
title: 'Hałas klimatyzatora — jak zmierzyć i co oznaczają decybele?',
category: 'porady',
excerpt: 'Cichy klimatyzator do sypialni – na co patrzeć w karcie katalogowej? Tłumaczymy skalę decybeli i sprawdzamy dlaczego jednostka zewnętrzna budzi sąsiadów.',
date: '2026-06-12T10:00:00Z',
author: 'Janusz Nowak',
image: '',
content: `
<h2>Cisza to luksus. Jak jej nie zepsuć?</h2>
<p>Kupując klimatyzator, najczęściej patrzymy na moc, design i cenę. Hałas zauważamy dopiero, gdy urządzenie burzy nam sen w gorącą noc. Specyfikacje sprzętu wyrażają natężenie dźwięku w dB(A) (decybelach akustycznych), których skala jest logarytmiczna (każde 3 dB to podwojenie mocy dźwięku!).</p>
<h3>Głośność wewnątrz (sypialnia i salon)</h3>
<p>Idealny klimatyzator w sypialni powinien na najniższym biegu (tryb sleep) generować hałas od 18 dB do 22 dB. Taki dźwięk jest porównywalny do szeptu na ucho lub szumu liści i uważa się go za niesłyszalny. W salonie dopuszczalne jest 25-30 dB.</p>
<h3>Głośność agregatu a prawo</h3>
<ul>
<li>Dopuszczalny hałas na granicy działki w zabudowie jednorodzinnej to max 50 dB w dzień i 40 dB w nocy.</li>
<li>Aby urządzenie za oknem nie obudziło sąsiadów zza miedzy, szukaj agregatów wyposażonych w sprężarki inwerterowe o mocy akustycznej poniżej 60 dB oraz montuj je na amortyzatorach (wibroizolatorach gumowych).</li>
</ul>
<p>Zawsze przed doborem sprzętu analizujemy ryzyko nadmiernego hałasu u Ciebie i Twoich sąsiadów. Bezpieczeństwo i święty spokój w cenie!</p>
`
},
{
slug: 'pompa-ciepla-ile-kosztuje-eksploatacja',
title: 'Ile kosztuje eksploatacja pompy ciepła? Realne rachunki za prąd',
category: 'pompy-ciepla',
excerpt: 'Wszyscy mówią, że pompy ciepła są tanie, ale co to znaczy w praktyce? Rozbijamy na czynniki pierwsze roczne rachunki z realnych instalacji w 2026 r.',
date: '2026-06-15T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
content: `
<h2>Realne koszty w miejsce marketingu</h2>
<p>Producenci w katalogach operują teoretycznymi wskaźnikami COP (często zbadanymi w laboratoryjnych +7°C). My spójrzmy na realne zużycie z instalacji, które wykonaliśmy dla naszych klientów.</p>
<h3>Przykład z życia: dom 150 m², nowe budownictwo</h3>
<p>Dom wyposażony we w pełni sprawną instalację podłogową, zamieszkały przez 4 osobową rodzinę. Zastosowano pompę ciepła powietrze-woda o mocy 8 kW. Urządzenie dostarcza zarówno C.O., jak i CWU (ciepła woda użytkowa do mycia).</p>
<ul>
<li>Roczne zapotrzebowanie domu na ciepło: 12 000 kWh.</li>
<li>Średni sezonowy współczynnik (SCOP) instalacji wynosi 3.5.</li>
<li>Roczne pobranie prądu z sieci: ok. 3400 kWh.</li>
<li>Koszt przy uśrednionej taryfie: <strong>ok. 3000 zł rocznie (250 zł / miesiąc)</strong>.</li>
</ul>
<p>Aby jeszcze bardziej zbić rachunki, wielu naszych klientów zasila pompę prądem ze słońca (instalacja PV). Chcesz wiedzieć ile zapłacisz Ty? Udostępniamy kalkulator eksploatacji przy wycenie każdego zlecenia.</p>
`
},
{
slug: 'klimatyzacja-vs-wentylacja-roznice',
title: 'Klimatyzacja vs wentylacja rekuperacyjna — co wybrać do domu?',
category: 'porady',
excerpt: 'Mylenie klimatyzacji z rekuperacją to częsty błąd. Wyjaśniamy dlaczego chłodzenie to nie oddychanie, i dlaczego najlepiej wdrożyć oba systemy naraz.',
date: '2026-06-20T10:00:00Z',
author: 'Marta Wiśniewska',
image: '',
content: `
<h2>Chłodne, czy świeże powietrze?</h2>
<p>Klimatyzacja chłodzi, grzeje, osusza i filtruje. Czego jednak NIE ROBI standardowa klimatyzacja ścienna? <strong>Nie wymienia powietrza na nowe!</strong> Cały proces w systemach Split polega na "mieleniu" tego samego powietrza zamkniętego w Twoim pokoju. Bez przewietrzenia, w sypialni po paru godzinach zacznie drastycznie rosnąć stężenie CO2.</p>
<h3>Rozwiązanie: Wentylacja mechaniczna z rekuperacją</h3>
<p>Rekuperacja to absolutny fundament zdrowego domu. Wyciąga ona zużyte, brudne powietrze bogate w CO2, a nawiewa z zewnątrz natlenione powietrze wyczyszczone z kurzu. Wymiennik ciepła w centrali pozwala na ogrzanie zimnego nawiewu (zimą) lub delikatne jego schłodzenie (latem).</p>
<h3>Dlaczego łączyć oba systemy?</h3>
<ul>
<li>Rekuperacja dba o 100% dostępu do tlenu bez strat ciepła.</li>
<li>Klimatyzacja włącza się tylko przy największych upałach, chłodząc dom szybko i ekstremalnie sprawnie.</li>
</ul>
<p>W budownictwie w 2026 roku nie ma powrotu do wentylacji grawitacyjnej (komina). Nowoczesny, hermetyczny dom potrzebuje sztucznych płuc (rekuperacji) i komfortu (klimatyzacji). Budujesz dom? Pomożemy dobrać kompleksowy system HVAC dla całej rodziny!</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 getFeaturedPosts = (limit = 3) => posts.filter((p) => p.featured).slice(0, limit);
export const getPostsByCategory = (category: string) => posts.filter((p) => p.category === category);
export const getRecentPosts = (limit = 6) => [...posts].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, limit);
// Dodane z powrotem funkcje wywoływane w komponentach, które nie zostały uwzględnione w prompcie:
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);
export const getLatestPosts = (limit = 3) => getRecentPosts(limit);
export const getRelatedPosts = (slug: string, category: string, limit = 2) => {
return posts.filter(p => p.slug !== slug && p.category === category).slice(0, limit);
};
+1 -22
View File
@@ -112,25 +112,4 @@ export const heatPumpProcessSteps: ProcessStep[] = [
},
];
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: "Rozpoczynamy prace u klienta w umówionym terminie. Zabezpieczamy przestrzeń, dbamy o porządek i najwyższą jakość wykonania.",
},
{
number: 4,
title: "Odbiór i gwarancja",
description: "Wspólnie odbieramy wykonane prace. Na wszystkie usługi remontowo-instalacyjne udzielamy gwarancji.",
},
];
+7132 -156
View File
File diff suppressed because it is too large Load Diff
+1 -25
View File
@@ -2,13 +2,9 @@ export interface Project {
id: string;
title: string;
description: string;
category: 'klimatyzacja' | 'pompy-ciepla' | 'remonty' | 'hydraulika';
category: 'klimatyzacja' | 'pompy-ciepla';
location: string;
imageUrl: string;
beforeAfter?: {
beforeUrl: string;
afterUrl: string;
};
}
export const projectsData: Project[] = [
@@ -36,24 +32,4 @@ export const projectsData: Project[] = [
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',
}
];
+1 -19
View File
@@ -12,7 +12,7 @@ export const services: ServiceItem[] = [
title: "Serwis i przeglądy",
description: "Regularne przeglądy, czyszczenie, odgrzybianie oraz serwis naprawczy urządzeń klimatyzacyjnych.",
icon: "Wrench",
href: "/klimatyzacja",
href: "/klimatyzacja#serwis",
},
{
title: "Pompy ciepła",
@@ -20,22 +20,4 @@ export const services: ServiceItem[] = [
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",
},
];
+1 -7
View File
@@ -9,13 +9,7 @@ export const testimonials: Testimonial[] = [
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,
+19 -71
View File
@@ -2,16 +2,12 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface CartItem {
id: string; // Unikalne ID, połączenie slug + wybrane opcje
id: string;
slug: string;
name: string;
brand: string;
image: string;
power?: number;
label?: string; // Np. "2.5 kW" z wariantu
price: number; // Podstawowa cena za 1 szt. bez montażu
priceWithInstall: number; // Cena za 1 szt. z montażem
withInstall: boolean;
price: number;
quantity: number;
}
@@ -20,9 +16,9 @@ interface CartState {
addItem: (item: Omit<CartItem, 'id' | 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
toggleInstall: (id: string) => void;
clearCart: () => void;
getTotals: () => { subtotal: number; vat: number; total: number };
getTotal: () => number;
getCount: () => number;
}
export const useCartStore = create<CartState>()(
@@ -32,89 +28,41 @@ export const useCartStore = create<CartState>()(
addItem: (newItem) => {
set((state) => {
const id = `${newItem.slug}-${newItem.label || 'base'}-${newItem.withInstall}`;
const existingItem = state.items.find((item) => item.id === id);
if (existingItem) {
const id = newItem.slug;
const existing = state.items.find((i) => i.id === id);
if (existing) {
return {
items: state.items.map((item) =>
item.id === id ? { ...item, quantity: item.quantity + 1 } : item
items: state.items.map((i) =>
i.id === id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...newItem, id, quantity: 1 }] };
});
},
removeItem: (id) => {
set((state) => ({
items: state.items.filter((item) => item.id !== id),
}));
},
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
updateQuantity: (id, quantity) => {
if (quantity < 1) return;
set((state) => ({
items: state.items.map((item) =>
item.id === id ? { ...item, quantity } : item
),
items: state.items.map((i) => (i.id === id ? { ...i, quantity } : i)),
}));
},
toggleInstall: (id) => {
set((state) => {
const itemToUpdate = state.items.find(item => item.id === id);
if (!itemToUpdate) return state;
const newWithInstall = !itemToUpdate.withInstall;
const newId = `${itemToUpdate.slug}-${itemToUpdate.label || 'base'}-${newWithInstall}`;
const targetExists = state.items.find(item => item.id === newId);
if (targetExists) {
return {
items: state.items
.map(item => item.id === newId ? { ...item, quantity: item.quantity + itemToUpdate.quantity } : item)
.filter(item => item.id !== id)
}
}
return {
items: state.items.map((item) =>
item.id === id ? { ...item, withInstall: newWithInstall, id: newId } : item
),
};
});
},
clearCart: () => set({ items: [] }),
getTotals: () => {
getTotal: () => {
const { items } = get();
let subtotal = 0;
let vat = 0;
return items.reduce((sum, i) => sum + i.price * i.quantity, 0);
},
items.forEach((item) => {
const itemPrice = item.withInstall ? item.priceWithInstall : item.price;
const itemTotalGross = itemPrice * item.quantity;
const vatRate = item.withInstall ? 0.08 : 0.23;
const itemTotalNet = itemTotalGross / (1 + vatRate);
const itemVat = itemTotalGross - itemTotalNet;
subtotal += itemTotalNet;
vat += itemVat;
});
return {
subtotal,
vat,
total: subtotal + vat
};
getCount: () => {
const { items } = get();
return items.reduce((sum, i) => sum + i.quantity, 0);
},
}),
{
name: 'air-project-cart',
}
{ name: 'thermcool-cart' }
)
);
+35
View File
@@ -0,0 +1,35 @@
import { create } from 'zustand';
interface CompareItem {
slug: string;
name: string;
brand: string;
price: number | null;
image: string;
specs: Record<string, string>;
}
interface CompareState {
items: CompareItem[];
addItem: (item: CompareItem) => void;
removeItem: (slug: string) => void;
clearAll: () => void;
isAdded: (slug: string) => boolean;
}
export const useCompareStore = create<CompareState>()((set, get) => ({
items: [],
addItem: (item) => {
const { items } = get();
if (items.length >= 3 || items.find((i) => i.slug === item.slug)) return;
set({ items: [...items, item] });
},
removeItem: (slug) =>
set((state) => ({ items: state.items.filter((i) => i.slug !== slug) })),
clearAll: () => set({ items: [] }),
isAdded: (slug) => get().items.some((i) => i.slug === slug),
}));
+7 -12
View File
@@ -1,35 +1,30 @@
export interface Product {
slug: string;
sku: string;
name: string;
brand: string;
category: "scienne" | "multisplit" | "przenosne" | "kasetonowe" | "konsolowe";
power: number; // kW
price: number; // cena urządzenia brutto (23% VAT)
priceWithInstall: number; // cena z montażem brutto (8% VAT dla os. prywatnych)
variants?: { label: string; power: number; price: number; priceWithInstall?: number }[];
category: 'scienne' | 'multisplit';
price: number | null;
image: string;
features: string[]; // np. ["Wi-Fi", "jonizacja", "grzanie"]
heating: boolean;
description: string;
specs: Record<string, string>; // klucz: wartość
shortDescription: string;
specs: Record<string, string>;
}
export interface Project {
slug: string;
title: string;
category: "klimatyzacja" | "pompy-ciepla" | "remonty" | "hydraulika";
category: "klimatyzacja" | "pompy-ciepla";
location: string; // np. "Wrocław"
image: string;
images?: string[]; // galeria
description: string;
scope?: string[]; // zakres prac
beforeAfter?: { before: string; after: string }; // dla remontów
}
export interface Post {
slug: string;
title: string;
category: "klimatyzacja" | "pompy-ciepla" | "remonty" | "hydraulika" | "porady";
category: "klimatyzacja" | "pompy-ciepla" | "porady";
excerpt: string;
date: string; // ISO format string
author: string;