Zastosowanie wszystkich poprawek UI i logiki kalkulatora

This commit is contained in:
AI Assistant
2026-08-10 23:26:52 +02:00
commit 7324a6f800
218 changed files with 33628 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
import React from 'react';
import Image from 'next/image';
import { SectionHeading } from '../common/SectionHeading';
import { CheckCircle2 } from 'lucide-react';
import { companyDetails } from '@/content/company';
export function AboutStory() {
return (
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
{/* Lewa: Obraz */}
<div className="relative">
<div className="relative aspect-[4/5] rounded-3xl overflow-hidden shadow-2xl">
<Image
src="/images/about/about-story.webp"
alt="Zespół firmy w trakcie pracy"
fill
className="object-cover"
/>
</div>
{/* Ozdobny element zaufania */}
<div className="absolute -bottom-8 -right-8 bg-white p-6 rounded-2xl shadow-xl hidden md:block">
<div className="flex items-center gap-4">
<div className="flex-shrink-0 w-16 h-16 bg-accent/10 rounded-full flex items-center justify-center">
<span className="text-2xl font-extrabold text-accent font-heading">
{companyDetails.stats.find(s => s.label.includes("doświadczenia"))?.value || "15+"}
</span>
</div>
<div>
<p className="font-bold text-primary font-heading text-lg leading-none mb-1">Lat na rynku</p>
<p className="text-sm text-text-muted">Doświadczenie i zaufanie</p>
</div>
</div>
</div>
</div>
{/* Prawa: Tekst */}
<div>
<SectionHeading
eyebrow="Nasza Historia"
title="Budujemy komfort od fundamentów aż po dach"
align="left"
/>
<div className="prose prose-lg text-text-muted mt-8 mb-10">
<p>
Firma {companyDetails.name} powstała z pasji do nowoczesnych technologii oraz chęci dostarczania kompleksowych usług dla domów i firm. Zaczynaliśmy jako niewielka, dwuosobowa ekipa specjalizująca się w instalacjach HVAC. Dziś jesteśmy liderem w regionie, realizując setki zaawansowanych projektów instalacyjnych rocznie.
</p>
<p>
Naszym celem jest upraszczanie życia naszym klientom. Zamiast szukać oddzielnych fachowców od hydrauliki, klimatyzacji i wykończeń, u nas otrzymujesz całościowe wsparcie z gwarancją jakości.
</p>
</div>
<ul className="space-y-4">
{[
"Certyfikowani instalatorzy z uprawnieniami F-Gaz",
"Dbałość o czystość podczas prac montażowych",
"Gwarancja i szybki serwis na terenie miasta i okolic",
"Pomoc w uzyskiwaniu dofinansowań"
].map((point, idx) => (
<li key={idx} className="flex items-start gap-3">
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
<span className="text-primary font-medium">{point}</span>
</li>
))}
</ul>
</div>
</div>
</section>
);
}
+85
View File
@@ -0,0 +1,85 @@
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Wind, LayoutGrid, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
const acTypes = [
{
title: 'Klimatyzatory SPLIT',
image: '/images/services/split.webp',
description: 'Najpopularniejsze rozwiązanie do jednego pomieszczenia. Składa się z jednej jednostki wewnętrznej i jednej zewnętrznej.',
icon: Wind,
subtypes: ['Ścienne', 'Konsolowe', 'Kanałowe'],
link: '/sklep?category=scienne',
},
{
title: 'Systemy MULTISPLIT',
image: '/images/services/split.webp',
description: 'Idealne do chłodzenia całego domu. Do jednej jednostki zewnętrznej można podłączyć od 2 do 5 jednostek wewnętrznych.',
icon: LayoutGrid,
subtypes: ['Dla 2 pomieszczeń', 'Dla 3 pomieszczeń', 'Dla 4-5 pomieszczeń', 'Mieszane typy jednostek'],
link: '/sklep?category=multisplit',
},
/* {
title: 'Pozostałe rozwiązania',
image: '/images/services/kasetonowe.webp',
description: 'Specjalistyczne urządzenia łączące funkcje chłodzenia z zaawansowanym grzaniem lub oczyszczaniem powietrza.',
icon: Sparkles,
subtypes: ['Z funkcją ogrzewania (do -25°C)', 'Z zaawansowanym oczyszczaczem', 'Klimatyzatory przenośne', 'Klimatyzatory bez jednostki zewn.'],
link: '/sklep',
}, */
];
export const AcTypesSection = () => {
return (
<section className="py-16 bg-gray-50">
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
<div className="text-center max-w-2xl mx-auto mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Rodzaje klimatyzatorów</h2>
<p className="text-gray-600">
Dobierzemy rozwiązanie idealnie dopasowane do Twoich potrzeb, niezależnie od tego czy chłodzisz jedno pomieszczenie, czy cały dom.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{acTypes.map((type, idx) => {
const Icon = type.icon;
return (
<div key={idx} className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 flex flex-col h-full hover:shadow-md transition-shadow">
<div className="relative w-full h-44 rounded-xl overflow-hidden mb-4">
<Image src={type.image} alt={type.title} fill className="object-cover" />
</div>
<div className="w-14 h-14 bg-accent/10 rounded-xl flex items-center justify-center mb-6 text-accent">
<Icon className="w-7 h-7" />
</div>
<h3 className="text-xl font-bold text-primary mb-3">{type.title}</h3>
<p className="text-gray-600 mb-6 text-sm flex-grow">
{type.description}
</p>
<ul className="space-y-2 mb-8">
{type.subtypes.map((subtype, sIdx) => (
<li key={sIdx} className="flex items-center text-sm text-gray-700">
<div className="w-1.5 h-1.5 rounded-full bg-accent mr-3"></div>
{subtype}
</li>
))}
</ul>
<Link
href={type.link}
className={cn("btn btn-outline btn-primary bg-white w-full justify-between group mt-auto")}
>
Sprawdź modele
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
);
})}
</div>
</div>
</section>
);
};
+26
View File
@@ -0,0 +1,26 @@
import { CheckCircle2, Zap, ShieldCheck } from "lucide-react";
export function AdvantagesBar() {
const advantages = [
{ icon: Zap, text: "Certyfikat F-Gaz" },
{ icon: ShieldCheck, text: "Gwarancja na sprzęt i montaż" },
{ icon: CheckCircle2, text: "Bezpłatna wycena w 24h" },
];
return (
<div className="bg-primary text-primary-foreground py-6 border-y border-border/10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6">
{advantages.map((adv, idx) => (
<div key={idx} className="flex items-center justify-center md:justify-start gap-3">
<adv.icon className="size-5 text-accent shrink-0" />
<span className="text-sm font-medium leading-tight">
{adv.text}
</span>
</div>
))}
</div>
</div>
</div>
);
}
+57
View File
@@ -0,0 +1,57 @@
import React from 'react';
import { TrendingDown, Leaf, ThermometerSun, Wallet } from 'lucide-react';
const benefits = [
{
title: 'Znaczne oszczędności',
description: 'Nawet 70% niższe koszty ogrzewania w porównaniu do tradycyjnych źródeł ciepła, takich jak węgiel, olej czy gaz.',
icon: TrendingDown,
},
{
title: 'Ekologiczne ciepło',
description: 'Brak emisji spalin i zanieczyszczeń w miejscu działania. Pompa w 75% korzysta z odnawialnej energii z otoczenia.',
icon: Leaf,
},
{
title: 'Komfort całoroczny',
description: 'Bezobsługowy system zapewniający przyjemne ciepło zimą oraz chłodzenie i produkcję ciepłej wody użytkowej latem.',
icon: ThermometerSun,
},
{
title: 'Dotacje i ulgi',
description: 'Możliwość odliczenia od podatku oraz skorzystania z programów Czyste Powietrze lub Mój Prąd na pokrycie części kosztów.',
icon: Wallet,
},
];
export const BenefitsGrid = () => {
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
<div className="text-center max-w-2xl mx-auto mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Dlaczego warto wybrać pompę ciepła?</h2>
<p className="text-gray-600">
Pompa ciepła to obecnie najbardziej efektywne i przyszłościowe rozwiązanie do ogrzewania budynków.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{benefits.map((item, idx) => {
const Icon = item.icon;
return (
<div key={idx} className="bg-gray-50 border border-gray-100 rounded-2xl p-8 hover:shadow-lg transition-shadow">
<div className="w-14 h-14 bg-accent/10 rounded-2xl flex items-center justify-center mb-6 text-accent">
<Icon className="w-7 h-7" />
</div>
<h3 className="text-xl font-bold text-primary mb-3">{item.title}</h3>
<p className="text-gray-600 text-sm leading-relaxed">
{item.description}
</p>
</div>
);
})}
</div>
</div>
</section>
);
};
+108
View File
@@ -0,0 +1,108 @@
import Link from "next/link";
import Image from "next/image";
import { ArrowRight, Calendar } from "lucide-react";
import { SectionHeading } from "../common/SectionHeading";
import { getLatestPosts } from "@/content/posts";
export function BlogPreview() {
const latestPosts = getLatestPosts(3);
const formatDate = (dateStr: string) => {
const d = new Date(dateStr);
return d.toLocaleDateString("pl-PL", {
year: "numeric",
month: "long",
day: "numeric",
});
};
const getCategoryLabel = (cat: string) => {
const labels: Record<string, string> = {
klimatyzacja: "Klimatyzacja",
porady: "Porady ogólne",
};
return labels[cat] || cat;
};
return (
<section className="py-16 md:py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Aktualności"
title="Najnowsze poradniki i artykuły"
subtitle="Dzielimy się wiedzą i doświadczeniem. Zobacz nasze wpisy dotyczące klimatyzacji i pomp ciepła oraz oszczędzania energii."
align="center"
/>
{/* Blog Posts Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{latestPosts.map((post) => (
<article
key={post.slug}
className="group flex flex-col justify-between border border-border rounded-xl overflow-hidden hover:shadow-md transition-shadow h-full"
>
<div>
{/* Thumbnail */}
<div className="relative aspect-video bg-slate-50 border-b border-border">
<Image
src={post.image}
alt={post.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 33vw"
/>
</div>
{/* Content */}
<div className="p-6">
{/* Category & Date */}
<div className="flex items-center gap-4 text-xs text-text-muted mb-3">
<span className="font-semibold uppercase tracking-wider text-accent">
{getCategoryLabel(post.category)}
</span>
<span className="flex items-center gap-1">
<Calendar className="size-3.5" />
<span>{formatDate(post.date)}</span>
</span>
</div>
{/* Title */}
<h3 className="text-lg font-bold text-primary font-heading line-clamp-2 leading-snug group-hover:text-accent transition-colors">
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</h3>
{/* Excerpt */}
<p className="text-sm text-text-muted mt-3 line-clamp-2 leading-relaxed">
{post.excerpt}
</p>
</div>
</div>
{/* Read More Link */}
<div className="p-6 pt-0 mt-auto">
<Link
href={`/blog/${post.slug}`}
className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary group-hover:text-accent transition-colors"
>
<span>Czytaj więcej</span>
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</article>
))}
</div>
{/* Blog Catalog Button */}
<div className="flex justify-center mt-12">
<Link href="/blog" className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 group">
<span>Przejdź do bloga</span>
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
</section>
);
}
+41
View File
@@ -0,0 +1,41 @@
import React from 'react';
import Link from 'next/link';
const brands = [
{ name: 'Daikin', logo: '/brands/daikin.webp' },
{ name: 'Mitsubishi Electric', logo: '/brands/mitsubishi.webp' },
{ name: 'Gree', logo: '/brands/gree.webp' },
{ name: 'Rotenso', logo: '/brands/rotenso.webp' },
{ name: 'LG', logo: '/brands/lg.webp' },
{ name: 'Samsung', logo: '/brands/samsung.webp' },
{ name: 'Haier', logo: '/brands/haier.webp' },
{ name: 'CHV', logo: '/brands/chv.webp' },
{ name: 'Toshiba', logo: '/brands/toshiba.webp' },
{ name: 'Fuji Electric', logo: '/brands/fuji.webp' },
];
export const BrandsGrid = () => {
return (
<section className="py-16 bg-white border-t border-border">
<div className="container mx-auto px-4 md:px-8 max-w-5xl text-center">
<h3 className="text-xl font-semibold text-text-muted mb-8 uppercase tracking-wider text-sm">
Autoryzowany partner czołowych producentów
</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4 lg:gap-6 items-center">
{brands.map((brand, idx) => (
<Link
key={idx}
href={`/sklep?brand=${encodeURIComponent(brand.name)}`}
className="group flex justify-center items-center h-16 bg-slate-50 rounded-xl p-3 border border-border hover:border-accent hover:bg-accent/5 hover:shadow-sm transition-all duration-300"
>
{/* Tutaj docelowo można dać tag Image */}
<span className="font-bold text-primary text-sm md:text-base tracking-tight opacity-70 group-hover:opacity-100 group-hover:text-accent transition-all">{brand.name}</span>
</Link>
))}
</div>
</div>
</section>
);
};
@@ -0,0 +1,37 @@
import React from 'react';
import { SectionHeading } from '../common/SectionHeading';
import { companyDetails } from '@/content/company';
export function CertificatesSection() {
return (
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Uprawnienia"
title="Nasze certyfikaty"
subtitle="Zaufaj wykwalifikowanym specjalistom. Posiadamy wszystkie licencje wymagane do profesjonalnego montażu urządzeń HVAC."
align="center"
/>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-10 mt-16">
{companyDetails.certificates.map((cert, idx) => (
<div
key={idx}
className="group flex flex-col items-center text-center gap-4 p-6 bg-slate-50 rounded-2xl border border-border hover:border-accent/50 transition-colors"
>
{/* W rzeczywistości użylibyśmy obrazków. Tutaj dajemy szary prostokąt jako placeholder wg wytycznych */}
<div className="relative w-full aspect-[4/3] bg-white rounded-lg border border-border flex items-center justify-center p-4 overflow-hidden group-hover:shadow-md transition-shadow">
{/* Jeśli cert.image istnieje realnie, można by użyć Image, na razie tekstowy placeholder */}
<div className="text-sm font-bold text-slate-300 font-heading">
LOGO / CERTYFIKAT
</div>
</div>
<p className="font-semibold text-primary text-sm leading-tight group-hover:text-accent transition-colors">
{cert.name}
</p>
</div>
))}
</div>
</section>
);
}
@@ -0,0 +1,77 @@
import React from 'react';
import Link from 'next/link';
import { cn } from '@/lib/utils';
import { Settings2, Zap, Palette, Calculator, Lightbulb } from 'lucide-react';
const steps = [
{ icon: Settings2, label: 'Marka i model' },
{ icon: Zap, label: 'Moc chłodnicza' },
{ icon: Palette, label: 'Design i funkcje' },
{ icon: Calculator, label: 'Wycena montażu' },
];
export const ConfiguratorTeaser = () => {
return (
<section className="py-20 bg-primary text-white relative overflow-hidden">
{/* Background decoration */}
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-accent/20 to-transparent pointer-events-none" />
<div className="absolute -bottom-24 -left-24 w-64 h-64 bg-accent rounded-full blur-[120px] opacity-20 pointer-events-none" />
<div className="container mx-auto px-4 md:px-8 max-w-7xl relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-3xl md:text-4xl font-bold mb-6">
Skonfiguruj swój idealny zestaw w 4 krokach
</h2>
<p className="text-gray-300 text-lg mb-8 max-w-xl">
Nie wiesz jakie urządzenie wybrać? Nasz prosty konfigurator krok po kroku pomoże Ci dobrać sprzęt i natychmiast sprawdzić szacunkowy koszt wraz z montażem.
</p>
<div className="flex flex-col sm:flex-row gap-4 mb-10 lg:mb-0">
<Link
href="/kalkulator"
className={cn("btn btn-primary text-white group")}
>
Uruchom konfigurator
</Link>
</div>
</div>
<div className="relative">
{/* Visual representation of steps */}
<div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-2xl p-8">
<div className="space-y-6 relative before:absolute before:inset-0 before:ml-6 before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-white/30 before:to-transparent">
{steps.map((step, idx) => {
const Icon = step.icon;
return (
<div key={idx} className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group">
<div className="flex items-center justify-center w-12 h-12 rounded-full border-4 border-primary bg-accent text-white shadow md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2 z-10">
<Icon className="w-5 h-5" />
</div>
<div className="w-[calc(100%-4rem)] md:w-[calc(50%-3rem)] bg-white/10 border border-white/20 p-4 rounded-xl backdrop-blur-sm group-hover:bg-white/20 transition-colors">
<h3 className="font-semibold text-lg">{step.label}</h3>
<p className="text-sm text-gray-300">Krok {idx + 1}</p>
</div>
</div>
);
})}
</div>
{/* Pro tip card */}
<div className="mt-8 bg-accent/20 border border-accent/40 rounded-xl p-4 flex items-start">
<Lightbulb className="w-6 h-6 text-accent shrink-0 mr-3 mt-0.5" />
<div>
<h4 className="font-semibold text-accent-light mb-1">Porada eksperta</h4>
<p className="text-sm text-gray-200">
Szybki przelicznik mocy: <strong>10 m² = 1 kW</strong> mocy chłodniczej. Dla pokoju 25 m² szukaj urządzenia o mocy ok. 2.5 kW.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
+90
View File
@@ -0,0 +1,90 @@
import React from 'react';
import { companyDetails } from '@/content/company';
import { ContactForm } from '../forms/ContactForm';
import { MapPin, Phone, Mail, Clock, FileText } from 'lucide-react';
export function ContactSection() {
return (
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24">
{/* Lewa kolumna: Dane kontaktowe */}
<div className="flex flex-col">
<h2 className="text-3xl md:text-4xl font-extrabold text-primary font-heading mb-6">
Porozmawiajmy o Twoim projekcie
</h2>
<p className="text-text-muted text-lg mb-12">
Jesteśmy do Twojej dyspozycji. Zadzwoń, napisz e-mail lub skorzystaj z formularza obok. Odpowiadamy najszybciej jak to możliwe, najczęściej w ciągu kilku godzin.
</p>
<div className="space-y-8 flex-grow">
{/* Telefon */}
<div className="flex items-start gap-4 group">
<div className="w-12 h-12 rounded-xl bg-accent/10 text-accent flex items-center justify-center shrink-0 group-hover:bg-accent group-hover:text-white transition-colors">
<Phone className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Zadzwoń do nas</p>
<a href={`tel:${companyDetails.phoneRaw}`} className="text-base font-bold text-primary hover:text-accent transition-colors font-heading">
{companyDetails.phone}
</a>
</div>
</div>
{/* Email */}
<div className="flex items-start gap-4 group">
<div className="w-12 h-12 rounded-xl bg-accent/10 text-accent flex items-center justify-center shrink-0 group-hover:bg-accent group-hover:text-white transition-colors">
<Mail className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Napisz e-mail</p>
<a href={`mailto:${companyDetails.email}`} className="text-base font-bold text-primary hover:text-accent transition-colors font-heading">
{companyDetails.email}
</a>
</div>
</div>
{/* Adres */}
<div className="flex items-start gap-4 group">
<div className="w-12 h-12 rounded-xl bg-accent/10 text-accent flex items-center justify-center shrink-0 group-hover:bg-accent group-hover:text-white transition-colors">
<MapPin className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Nasze biuro</p>
<div className="text-sm font-medium text-primary">
{companyDetails.name}<br />
{companyDetails.address.street}<br />
{companyDetails.address.zipCode} {companyDetails.address.city}
</div>
</div>
</div>
{/* Godziny i NIP */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8 pt-8 border-t border-border">
<div className="flex items-start gap-3">
<Clock className="w-5 h-5 text-primary/60 mt-0.5 shrink-0" />
<div>
<p className="text-sm font-semibold text-text-muted mb-1">Godziny pracy</p>
<p className="text-sm text-primary font-medium">{companyDetails.hours}</p>
</div>
</div>
<div className="flex items-start gap-3">
<FileText className="w-5 h-5 text-primary/60 mt-0.5 shrink-0" />
<div>
<p className="text-sm font-semibold text-text-muted mb-1">Dane firmy</p>
<p className="text-sm text-primary font-medium">NIP: {companyDetails.nip}</p>
</div>
</div>
</div>
</div>
</div>
{/* Prawa kolumna: Formularz */}
<div className="relative">
{/* Tło dekoracyjne */}
<div className="absolute inset-0 bg-primary/5 rounded-[2.5rem] transform translate-x-4 translate-y-4 -z-10 hidden sm:block" />
<ContactForm />
</div>
</div>
</section>
);
}
+89
View File
@@ -0,0 +1,89 @@
import Link from "next/link";
import { Phone, ArrowRight } from "lucide-react";
import { companyDetails } from "@/content/company";
import { cn } from "@/lib/utils";
interface CtaBannerProps {
title?: string;
subtitle?: string;
ctaText?: string;
ctaLink?: string;
bgVariant?: "primary" | "accent-light";
}
export function CtaBanner({
title = "Masz pytania? Umów darmową konsultację",
subtitle = "Skontaktuj się z nami! Doradzimy najlepsze rozwiązania, dobierzemy moc urządzeń i przygotujemy bezpłatną wycenę.",
ctaText = "Zamów bezpłatną konsultację",
ctaLink = "/kontakt",
bgVariant = "primary",
}: CtaBannerProps) {
const isPrimary = bgVariant === "primary";
return (
<section className="relative overflow-hidden py-16 md:py-20">
{/* Background and overlays */}
<div
className={cn(
"absolute inset-0 z-0",
isPrimary
? "bg-[#0F2A47] text-white"
: "bg-accent-light text-primary"
)}
/>
{isPrimary && (
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_left,rgba(43,168,224,0.12),transparent_40%)] z-0" />
)}
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col lg:flex-row items-center justify-between gap-10">
{/* Text Area */}
<div className="max-w-2xl text-center lg:text-left flex flex-col gap-3">
<h2
className={cn(
"text-2xl sm:text-3xl md:text-4xl font-extrabold font-heading leading-tight",
isPrimary ? "text-white" : "text-primary"
)}
>
{title}
</h2>
<p
className={cn(
"text-sm sm:text-base leading-relaxed max-w-xl",
isPrimary ? "text-slate-300" : "text-text-muted"
)}
>
{subtitle}
</p>
</div>
{/* Action buttons */}
<div className="flex flex-col sm:flex-row items-center gap-4 mt-8 w-full sm:w-auto shrink-0 justify-center">
<Link href={ctaLink} className="w-fit sm:w-auto btn btn-primary font-bold text-white px-6">
<span>{ctaText}</span>
<ArrowRight className="size-5" />
</Link>
<a
href={`tel:${companyDetails.phoneRaw}`}
className="w-fit sm:w-auto"
>
<button
className={cn(
"btn btn-outline btn-primary bg-white w-fit sm:w-auto font-bold flex items-center gap-2 px-6",
isPrimary
? "text-white hover:bg-white/20 hover:text-white border-white/30"
: "text-primary hover:bg-primary/10 border-primary/20"
)}
>
<Phone className="size-5" />
<span>{companyDetails.phone}</span>
</button>
</a>
</div>
</div>
</div>
</section>
);
}
+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import { PhoneCall, Clock, ShieldAlert } from 'lucide-react';
import { companyDetails } from '@/content/company';
export const EmergencyBlock = () => {
return (
<section className="py-12 bg-cta text-white relative overflow-hidden">
{/* Dekoracyjne tło */}
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-white/10 to-transparent pointer-events-none" />
<div className="absolute -bottom-24 -left-24 w-64 h-64 bg-white/20 rounded-full blur-[100px] pointer-events-none" />
<div className="container mx-auto px-4 md:px-8 max-w-5xl relative z-10">
<div className="flex flex-col md:flex-row items-center justify-between gap-8">
<div className="flex-1 text-center md:text-left">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-white/20 rounded-full text-sm font-semibold mb-4 backdrop-blur-sm border border-white/30">
<ShieldAlert className="w-4 h-4" />
Pogotowie hydrauliczne
</div>
<h2 className="text-3xl md:text-4xl font-bold mb-4">
Masz awarię? Zadzwoń natychmiast!
</h2>
<p className="text-white/90 text-lg mb-0 max-w-xl mx-auto md:mx-0">
Pęknięta rura, zatkany odpływ lub wyciek? Zapewniamy szybką diagnozę i skuteczną naprawę usterki.
</p>
</div>
<div className="flex flex-col items-center sm:items-end gap-4 shrink-0">
<a
href={`tel:${companyDetails.phone.replace(/\s+/g, '')}`}
className="group flex items-center gap-4 bg-white text-primary px-8 py-4 rounded-xl font-bold text-2xl hover:bg-gray-50 transition-colors shadow-lg hover:shadow-xl"
>
<div className="w-12 h-12 bg-cta/10 rounded-full flex items-center justify-center text-cta group-hover:scale-110 transition-transform">
<PhoneCall className="w-6 h-6" />
</div>
{companyDetails.phone}
</a>
<div className="flex items-center gap-2 text-sm font-medium bg-black/10 px-4 py-2 rounded-lg">
<Clock className="w-4 h-4 text-white/80" />
<span className="text-white/90">Szybki czas reakcji na terenie całego miasta</span>
</div>
</div>
</div>
</div>
</section>
);
};
+71
View File
@@ -0,0 +1,71 @@
'use client';
import React, { useState } from 'react';
import { faqData, FaqItem } from '@/content/faq';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
interface FaqAccordionProps {
category: FaqItem['category'];
}
export const FaqAccordion = ({ category }: FaqAccordionProps) => {
const [openId, setOpenId] = useState<string | null>(null);
const faqs = faqData.filter(item => item.category === category);
if (faqs.length === 0) return null;
return (
<section className="py-16 bg-gray-50">
<div className="container mx-auto px-4 md:px-8 max-w-3xl">
<div className="text-center mb-10">
<h2 className="text-3xl font-bold text-primary mb-4">Często zadawane pytania</h2>
<p className="text-gray-600">
Odpowiedzi na najczęstsze wątpliwości naszych klientów.
</p>
</div>
<div className="space-y-4">
{faqs.map((faq) => {
const isOpen = openId === faq.id;
return (
<div
key={faq.id}
className={cn(
"bg-white border rounded-xl overflow-hidden transition-all duration-200",
isOpen ? "border-accent shadow-md" : "border-gray-200 hover:border-gray-300"
)}
>
<button
className="w-full px-6 py-4 flex items-center justify-between text-left focus:outline-none"
onClick={() => setOpenId(isOpen ? null : faq.id)}
aria-expanded={isOpen}
>
<span className="font-semibold text-primary pr-4">{faq.question}</span>
<ChevronDown
className={cn(
"w-5 h-5 text-gray-400 transition-transform duration-200 shrink-0",
isOpen && "rotate-180 text-accent"
)}
/>
</button>
<div
className={cn(
"px-6 text-gray-600 leading-relaxed overflow-hidden transition-all duration-300 ease-in-out",
isOpen ? "max-h-96 pb-5 opacity-100" : "max-h-0 pb-0 opacity-0"
)}
>
<div className="pt-2 border-t border-gray-100">
{faq.answer}
</div>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
+80
View File
@@ -0,0 +1,80 @@
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Post } from '@/types/types';
import { Calendar, User, ArrowRight } from 'lucide-react';
interface FeaturedPostProps {
post: Post;
}
export function FeaturedPost({ post }: FeaturedPostProps) {
// Formatowanie daty
const dateObj = new Date(post.date);
const formattedDate = dateObj.toLocaleDateString('pl-PL', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
// Tłumaczenie kategorii
const categoryLabels: Record<string, string> = {
'klimatyzacja': 'Klimatyzacja',
'remonty': 'Remonty',
'hydraulika': 'Hydraulika',
'porady': 'Porady'
};
return (
<div className="group relative rounded-2xl overflow-hidden bg-white border border-border shadow-sm hover:shadow-lg transition-all duration-300">
<div className="flex flex-col md:flex-row">
{/* Obraz */}
<div className="relative w-full md:w-3/5 aspect-video md:aspect-auto md:min-h-[400px]">
<Image
src={post.image || '/images/hero/hero-section-image.webp'}
alt={post.title}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute top-4 left-4">
<span className="px-3 py-1 bg-accent text-white text-xs font-bold rounded-full uppercase tracking-wider shadow-md">
{categoryLabels[post.category] || post.category}
</span>
</div>
</div>
{/* Treść */}
<div className="p-8 md:p-12 flex flex-col justify-center w-full md:w-2/5">
<div className="flex items-center gap-4 text-text-muted text-sm font-medium mb-4">
<div className="flex items-center gap-1.5">
<Calendar className="w-4 h-4 text-primary/60" />
<span>{formattedDate}</span>
</div>
{post.author && (
<div className="flex items-center gap-1.5">
<User className="w-4 h-4 text-primary/60" />
<span>{post.author}</span>
</div>
)}
</div>
<h3 className="text-2xl md:text-3xl font-extrabold text-primary font-heading leading-tight mb-4 group-hover:text-accent transition-colors">
<Link href={`/blog/${post.slug}`} className="before:absolute before:inset-0">
{post.title}
</Link>
</h3>
<p className="text-text-muted leading-relaxed mb-8 line-clamp-3">
{post.excerpt}
</p>
<div className="flex items-center gap-2 text-primary font-semibold group-hover:text-accent transition-colors mt-auto">
<span>Czytaj pełny artykuł</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</div>
</div>
</div>
</div>
);
}
+39
View File
@@ -0,0 +1,39 @@
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { SectionHeading } from "../common/SectionHeading";
import { ProductCard } from "../shop/ProductCard";
import { getFeaturedProducts } from "@/content/products";
export function FeaturedProducts() {
const featured = getFeaturedProducts(4);
return (
<section className="py-16 md:py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Nasz Asortyment"
title="Polecane klimatyzatory z montażem"
subtitle="Poznaj wybrane modele urządzeń chłodzących i grzewczych od wiodących marek. Zapewniamy kompleksowy montaż."
align="center"
/>
{/* Products Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
{featured.map((product) => (
<ProductCard key={product.slug} product={product} />
))}
</div>
{/* Catalog Link */}
<div className="flex justify-center mt-12">
<Link href="/sklep" className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 group">
<span>Zobacz cały sklep</span>
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
</section>
);
}
+93
View File
@@ -0,0 +1,93 @@
import React from 'react';
import Link from 'next/link';
import { cn } from '@/lib/utils';
import { PiggyBank, FileText, CheckCircle2 } from 'lucide-react';
export const FundingSection = () => {
return (
<section className="py-16 bg-accent-light text-primary">
<div className="container mx-auto px-4 md:px-8 max-w-6xl relative">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<div className="inline-flex items-center gap-2 px-3 py-1 bg-white/50 backdrop-blur-sm rounded-full text-sm font-semibold mb-6">
<PiggyBank className="w-4 h-4 text-accent" />
Dotacje i dofinansowania
</div>
<h2 className="text-3xl md:text-4xl font-bold mb-6">
Odbierz nawet do 35 200 zł dotacji na pompę ciepła
</h2>
<p className="text-primary/80 text-lg mb-8">
[PLACEHOLDER - DO AKTUALIZACJI] Obecnie trwają programy rządowe &quot;Czyste Powietrze&quot; oraz &quot;Mój Prąd&quot;, które pozwalają na zwrot znacznej części kosztów inwestycji. Pomożemy Ci przejść przez cały proces formalny i przygotować niezbędne wnioski.
</p>
<ul className="space-y-4 mb-10">
<li className="flex items-start gap-3">
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
<span className="font-medium">Bezpłatny audyt możliwości dofinansowania</span>
</li>
<li className="flex items-start gap-3">
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
<span className="font-medium">Pomoc w wypełnieniu dokumentacji</span>
</li>
<li className="flex items-start gap-3">
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
<span className="font-medium">Możliwość prefinansowania inwestycji</span>
</li>
</ul>
<Link
href="/dofinansowania"
className={cn("btn btn-primary group")}
>
Dowiedz się więcej o dotacjach
</Link>
<p className="text-xs text-primary/60 mt-4 max-w-sm">
* Wysokość dotacji uzależniona jest od progu dochodowego i zakresu termomodernizacji. Skontaktuj się z nami w celu dokładnej wyceny.
</p>
</div>
<div className="relative">
{/* Dekoracyjna karta */}
<div className="bg-white rounded-3xl p-8 shadow-xl relative z-10 border border-white/50">
<div className="flex justify-between items-start mb-8">
<div>
<p className="text-sm text-gray-500 font-semibold mb-1">Twój kosztorys z dotacją</p>
<h3 className="text-xl font-bold text-primary">Przykładowa kalkulacja</h3>
</div>
<div className="w-12 h-12 bg-accent/10 rounded-full flex items-center justify-center">
<FileText className="w-6 h-6 text-accent" />
</div>
</div>
<div className="space-y-4 text-sm">
<div className="flex justify-between pb-4 border-b border-gray-100">
<span className="text-gray-600">Wartość inwestycji</span>
<span className="font-semibold text-gray-900">45 000 zł</span>
</div>
<div className="flex justify-between pb-4 border-b border-gray-100 text-green-600 font-medium">
<span>Dotacja &quot;Czyste Powietrze&quot;</span>
<span>- 20 500 zł</span>
</div>
<div className="flex justify-between pb-4 border-b border-gray-100 text-green-600 font-medium">
<span>Ulga termomodernizacyjna</span>
<span>- 4 600 zł</span>
</div>
<div className="flex justify-between pt-2">
<span className="text-gray-900 font-bold">Ostateczny koszt inwestora</span>
<span className="font-bold text-2xl text-primary">19 900 zł</span>
</div>
</div>
</div>
{/* Background elements */}
<div className="absolute -top-6 -right-6 w-full h-full bg-accent/20 rounded-3xl -z-10 blur-sm"></div>
<div className="absolute -bottom-8 -left-8 w-32 h-32 bg-white/40 rounded-full -z-10 blur-xl"></div>
</div>
</div>
</div>
</section>
);
};
+63
View File
@@ -0,0 +1,63 @@
import React from 'react';
import { Wind, Droplets, Mountain } from 'lucide-react';
const pumpTypes = [
{
title: 'Powietrze - Woda',
description: 'Najpopularniejsze rozwiązanie. Urządzenie pobiera energię z powietrza zewnętrznego i przekazuje ją do wodnej instalacji grzewczej (kaloryfery, podłogówka) oraz podgrzewa wodę użytkową.',
forWho: 'Idealne do nowych domów i większości termomodernizacji (wymiana pieca).',
icon: Droplets,
},
{
title: 'Powietrze - Powietrze',
description: 'Działają na zasadzie klimatyzatorów. Ogrzewają bezpośrednio powietrze w pomieszczeniu, bez udziału instalacji wodnej. Szybko nagrzewają i posiadają funkcję chłodzenia.',
forWho: 'Świetne jako dodatkowe źródło ciepła, do domków letniskowych i mniejszych przestrzeni.',
icon: Wind,
},
{
title: 'Gruntowe',
description: 'Pobierają ciepło z wnętrza ziemi za pomocą kolektorów. Zapewniają najwyższą i najbardziej stabilną wydajność niezależnie od mrozów na zewnątrz.',
forWho: 'Dla inwestorów z dużą działką, oczekujących najwyższej niezawodności i gotowych na wyższe koszty początkowe.',
icon: Mountain,
},
];
export const HeatPumpTypes = () => {
return (
<section className="py-16 bg-gray-50">
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
<div className="text-center max-w-2xl mx-auto mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Rodzaje pomp ciepła</h2>
<p className="text-gray-600">
Wybór odpowiedniej technologii zależy od uwarunkowań Twojego budynku, wielkości działki oraz budżetu.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{pumpTypes.map((type, idx) => {
const Icon = type.icon;
return (
<div key={idx} className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 flex flex-col h-full hover:shadow-md transition-shadow">
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-12 bg-primary/5 rounded-xl flex items-center justify-center text-primary">
<Icon className="w-6 h-6" />
</div>
<h3 className="text-xl font-bold text-primary">{type.title}</h3>
</div>
<p className="text-gray-600 mb-6 text-sm flex-grow">
{type.description}
</p>
<div className="bg-gray-50 rounded-lg p-4 mt-auto">
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Dla kogo?</h4>
<p className="text-sm text-gray-700 font-medium">{type.forWho}</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
+101
View File
@@ -0,0 +1,101 @@
import Link from "next/link";
import Image from "next/image";
import { ArrowRight, Star } from "lucide-react";
import { buttonClasses } from "@/components/ui/Button";
export interface HeroSectionProps {
eyebrow?: string;
title?: React.ReactNode;
subtitle?: string;
ctaText?: string;
ctaLink?: string;
secondaryCtaText?: string;
secondaryCtaLink?: string;
backgroundImage?: string;
features?: string[];
}
export function HeroSection({
eyebrow = "Wrocław i Dolny Śląsk",
title = (
<>
<span className="text-white">Klimatyzacja</span><br />
<span className="text-accent">na Dolnym Śląsku</span>
</>
),
subtitle = "Montaż, serwis i naprawa — przyjeżdżamy do Ciebie. Obsługujemy tam gdzie inni odmawiają, w tym klimatyzatory LG.",
ctaText = "Darmowa wycena",
ctaLink = "/kontakt",
secondaryCtaText = "Zobacz ofertę",
secondaryCtaLink = "/sklep",
backgroundImage = "/images/hero/home_page_hero.webp",
features
}: HeroSectionProps = {}) {
return (
<section className="relative bg-primary min-h-[480px] md:min-h-[600px] flex items-center overflow-hidden w-full">
{/* TODO ZDJĘCIE HERO: realne zdjęcie firmy/montażu, format poziomy min. 1920×1080, plik public/hero.jpg */}
<Image
src={backgroundImage}
alt="Montaż klimatyzacji i pomp ciepła"
fill
priority
sizes="100vw"
className="object-cover"
/>
{/* NAKŁADKA POD TEKST - jednolita, przyciemniająca */}
<div className="absolute inset-0 bg-black/50" />
<div className="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 md:py-24">
<div className="flex flex-col items-start text-left max-w-3xl">
{/* Badge */}
{eyebrow && (
<div className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full bg-accent/20 border border-accent/30 text-white text-xs font-semibold uppercase tracking-wider mb-6 font-heading">
<span className="size-2 rounded-full bg-accent" />
{eyebrow}
</div>
)}
{/* Heading */}
<h1 className="text-3xl md:text-5xl lg:text-6xl font-extrabold tracking-tight font-heading leading-[1.1] text-white">
{title}
</h1>
{/* Subtitle */}
<p className="mt-6 text-base md:text-lg lg:text-xl text-white/90 max-w-2xl leading-relaxed">
{subtitle}
</p>
{/* Optional Features List */}
{features && features.length > 0 && (
<ul className="mt-6 grid grid-cols-2 gap-3 text-white/90 font-medium">
{features.map((feature, idx) => (
<li key={idx} className="flex items-center gap-2">
<Star className="size-4 text-yellow-400 fill-yellow-400" />
{feature}
</li>
))}
</ul>
)}
{/* CTA Buttons */}
<div className="mt-10 flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto">
{ctaText && ctaLink && (
<Link href={ctaLink} className={buttonClasses({ variant: 'primary', size: 'md', className: 'w-fit sm:w-auto group' })}>
<span>{ctaText}</span>
<ArrowRight className="size-5 group-hover:translate-x-1 transition-transform" />
</Link>
)}
{secondaryCtaText && secondaryCtaLink && (
<Link href={secondaryCtaLink} className={buttonClasses({ variant: 'outline', size: 'md', className: 'w-fit sm:w-auto' })}>
<span>{secondaryCtaText}</span>
</Link>
)}
</div>
</div>
</div>
</section>
);
}
+151
View File
@@ -0,0 +1,151 @@
import Link from "next/link";
import { Wrench, Thermometer, Wind, Zap, Calendar, Phone, Check } from "lucide-react";
import { companyDetails } from "@/content/company";
import { buttonClasses } from "../ui/Button";
export function HomeServices() {
return (
<section className="bg-white py-16 px-4">
<div className="bg-slate-50 max-w-7xl mx-auto rounded-[2.5rem] p-6 sm:p-10 lg:p-14">
<div className="max-w-7xl mx-auto flex flex-col gap-8">
<div className="mb-2">
<div className="inline-flex items-center gap-1.5 text-[10px] font-bold text-accent uppercase tracking-widest mb-6 bg-accent/5 border border-accent/20 px-3 py-1.5 rounded-full">
<Check className="size-3.5" />
Cennik usług
</div>
<h2 className="text-3xl sm:text-5xl font-extrabold text-primary font-heading leading-tight mb-4">
Montaż, serwis i czyszczenie klimatyzacji –<br />już od <span className="underline decoration-accent underline-offset-4">150 zł</span>
</h2>
<p className="text-text-muted text-sm sm:text-base max-w-2xl leading-relaxed">
Profesjonalna obsługa, certyfikowani specjaliści i szybki czas reakcji na terenie całego Wrocławia. Zadbaj o czyste powietrze i wydajność swoich urządzeń.
</p>
</div>
{/* Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Card 1: Montaż klimatyzacji */}
<div className="bg-white p-8 flex flex-col rounded-2xl shadow-sm border border-border">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-100 flex items-center justify-center shrink-0 border border-slate-200 rounded-lg">
<Wrench className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">Montaż klimatyzacji</h3>
<p className="text-sm text-text-muted leading-relaxed">Profesjonalny montaż urządzeń split i multisplit. Zapewniamy estetykę wykonania i pełne uruchomienie.</p>
</div>
</div>
<div className="border-t border-border pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-1">Cena usługi</p>
<p className="text-xl font-extrabold text-primary">od 1400 zł</p>
<p className="text-[10px] text-text-muted mt-1">* cena standardowego montażu do 5m</p>
</div>
</div>
{/* Card 2: Czyszczenie */}
<div className="bg-white p-8 flex flex-col rounded-2xl shadow-sm border border-border">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-100 flex items-center justify-center shrink-0 border border-slate-200 rounded-lg">
<Thermometer className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">Czyszczenie i odgrzybianie</h3>
<p className="text-sm text-text-muted leading-relaxed">Kompleksowe czyszczenie, dezynfekcja chemiczna, odgrzybianie parowe, czyszczenie filtrów.</p>
</div>
</div>
<div className="border-t border-border pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-1">Cena usługi</p>
<p className="text-xl font-extrabold text-primary">od 250 zł</p>
</div>
</div>
{/* Card 3: Przegląd techniczny */}
<div className="bg-white p-8 flex flex-col rounded-2xl shadow-sm border border-border">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-100 flex items-center justify-center shrink-0 border border-slate-200 rounded-lg">
<Wind className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">Przegląd techniczny</h3>
<p className="text-sm text-text-muted leading-relaxed">Sprawdzenie parametrów pracy, kontrola szczelności układu, pomiar ciśnień roboczych.</p>
</div>
</div>
<div className="border-t border-border pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-1">Cena usługi</p>
<p className="text-xl font-extrabold text-primary">od 150 zł</p>
</div>
</div>
{/* Card 4: Uzupełnienie czynnika */}
<div className="bg-white p-8 flex flex-col rounded-2xl shadow-sm border border-border">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-100 flex items-center justify-center shrink-0 border border-slate-200 rounded-lg">
<Zap className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">Uzupełnienie czynnika</h3>
<p className="text-sm text-text-muted leading-relaxed">Lokalizacja nieszczelności, próba ciśnieniowa azotem, dobicie czynnika chłodniczego.</p>
</div>
</div>
<div className="border-t border-border pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-1">Cena usługi</p>
<p className="text-xl font-extrabold text-primary">od 180 zł</p>
</div>
</div>
{/* Card 5: Naprawa usterek */}
<div className="bg-white p-8 flex flex-col rounded-2xl shadow-sm border border-border">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-100 flex items-center justify-center shrink-0 border border-slate-200 rounded-lg">
<Wrench className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">Naprawa usterek</h3>
<p className="text-sm text-text-muted leading-relaxed">Diagnoza awarii, wymiana uszkodzonych podzespołów elektroniki, sprężarek, wentylatorów.</p>
</div>
</div>
<div className="border-t border-border pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-1">Cena usługi</p>
<p className="text-sm font-extrabold text-primary">Wycena indywidualna</p>
</div>
</div>
{/* Card 6: Serwis sezonowy (Dark card) */}
<div className="bg-[#1a2332] p-8 flex flex-col border border-[#1a2332] shadow-sm rounded-2xl">
<div className="flex flex-col gap-6 mb-12">
<div className="size-12 bg-slate-800 flex items-center justify-center shrink-0 border border-slate-700 rounded-lg">
<Calendar className="size-5 text-accent" />
</div>
<div>
<h3 className="text-xl font-bold text-white font-heading mb-3">Serwis sezonowy / Abonament</h3>
<p className="text-sm text-white/60 leading-relaxed">Regularna opieka nad instalacją przed sezonem letnim i zimowym. Priorytetowy czas reakcji w przypadku awarii.</p>
</div>
</div>
<div className="border-t border-white/10 pt-5 mt-auto">
<p className="text-[10px] font-bold uppercase tracking-widest text-white/60 mb-1">Cena usługi</p>
<p className="text-xl font-extrabold text-white">od 199 zł / rok</p>
</div>
</div>
{/* Card 6: Gotowy na świeże powietrze? (Full width) */}
<div className="bg-white md:col-span-3 p-8 md:p-10 flex flex-col lg:flex-row items-start lg:items-center justify-between gap-8 border-l-[6px] border-accent rounded-2xl shadow-sm">
<div className="max-w-2xl">
<h3 className="text-2xl font-bold text-primary font-heading mb-3">Gotowy na świeże powietrze?</h3>
<p className="text-sm text-text-muted leading-relaxed">Nasi certyfikowani technicy są dostępni od poniedziałku do soboty. Zarezerwuj termin już dziś, aby uniknąć kolejek w sezonie.</p>
</div>
<div className="flex flex-col sm:flex-row items-center gap-4 shrink-0 w-full lg:w-auto justify-center lg:justify-end">
<Link href="/kontakt" className={buttonClasses({ variant: 'primary', size: 'md' }) + ' w-fit sm:w-auto'}>
Umów serwis <Calendar className="size-4" />
</Link>
<Link href={`tel:${companyDetails.phoneRaw}`} className={buttonClasses({ variant: 'outline-light', size: 'md' }) + ' w-fit sm:w-auto'}>
<Phone className="size-4" /> Zadzwoń do nas
</Link>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import { Clock, Banknote, MapPin } from 'lucide-react';
interface InfoHighlightsProps {
description: string;
highlights: {
icon: 'clock' | 'price' | 'map';
title: string;
description: string;
}[];
}
const iconMap = {
clock: Clock,
price: Banknote,
map: MapPin,
};
export const InfoHighlights = ({ description, highlights }: InfoHighlightsProps) => {
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
<div className="lg:col-span-5">
<h2 className="text-3xl font-bold text-primary mb-6">Szybki montaż, jasne zasady</h2>
<p className="text-gray-600 text-lg leading-relaxed">
{description}
</p>
</div>
<div className="lg:col-span-7 grid grid-cols-1 sm:grid-cols-3 gap-6">
{highlights.map((item, idx) => {
const Icon = iconMap[item.icon];
return (
<div key={idx} className="bg-gray-50 p-6 rounded-xl border border-gray-100 flex flex-col items-center text-center">
<div className="w-12 h-12 bg-accent/10 rounded-full flex items-center justify-center mb-4 text-accent">
<Icon className="w-6 h-6" />
</div>
<h3 className="font-semibold text-primary mb-2">{item.title}</h3>
<p className="text-sm text-gray-500">{item.description}</p>
</div>
);
})}
</div>
</div>
</div>
</section>
);
};
+33
View File
@@ -0,0 +1,33 @@
import React from 'react';
export function MapEmbed() {
return (
<section className="w-full h-[400px] md:h-[500px] bg-slate-200 relative overflow-hidden">
{/*
Docelowo tutaj powinien znaleźć się kod iframe z Google Maps.
Przykład:
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2505.465597792618!2d17.031980815754593!3d51.10006297957077!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x0!2zNTHCsDA2JzAwLjIiTiAxN8KwMDInMDMuMCJF!5e0!3m2!1spl!2spl!4v1620000000000!5m2!1spl!2spl"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen={false}
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
className="absolute inset-0"
></iframe>
*/}
{/* Poniżej znajduje się Placeholder */}
<div className="absolute inset-0 flex items-center justify-center bg-slate-200 border-y border-border">
<div className="text-center p-6 bg-white/80 backdrop-blur-sm rounded-xl border border-white/50 shadow-sm max-w-sm mx-4">
<svg className="w-12 h-12 text-primary/40 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
<p className="font-semibold text-primary font-heading">Mapa Lokalizacji</p>
<p className="text-sm text-text-muted mt-1">Tutaj zostanie osadzona interaktywna mapa Google (iframe).</p>
</div>
</div>
</section>
);
}
+66
View File
@@ -0,0 +1,66 @@
import Link from "next/link";
import { ThermometerSun, Home, ChevronRight, ArrowRight } from "lucide-react";
export function PillarsSection() {
return (
<section className="py-16 md:py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Pillar 1: Chłód i Ogrzewanie */}
<div className="group relative rounded-2xl border border-border p-8 md:p-12 bg-slate-50 hover:bg-white hover:border-accent hover:shadow-xl transition-all duration-300 flex flex-col justify-between overflow-hidden">
{/* Visual background gradient glow on hover */}
<div className="absolute right-0 top-0 size-48 rounded-full bg-accent/5 filter blur-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div>
<div className="inline-flex p-4 rounded-xl bg-accent-light text-accent mb-6 group-hover:scale-110 transition-transform duration-300">
<ThermometerSun className="size-8" />
</div>
<h3 className="text-2xl md:text-3xl font-bold text-primary font-heading mb-4">
Chłód i Ogrzewanie
</h3>
<p className="text-text-muted text-base leading-relaxed mb-8 max-w-md">
Klimatyzacja dla Twojego domu lub biura. Oferujemy profesjonalny audyt, dobór optymalnej mocy, autoryzowany montaż i stałą opiekę serwisową.
</p>
</div>
<div>
<Link href="/klimatyzacja" className="btn btn-primary text-white flex items-center gap-2">
<span>Przejdź do oferty HVAC</span>
<ChevronRight className="size-5 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
{/* Pillar 2: Twój Dom */}
<div className="group relative rounded-2xl border border-border p-8 md:p-12 bg-slate-50 hover:bg-white hover:border-cta hover:shadow-xl transition-all duration-300 flex flex-col justify-between overflow-hidden">
{/* Visual background gradient glow on hover */}
<div className="absolute right-0 top-0 size-48 rounded-full bg-cta/5 filter blur-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div>
<div className="inline-flex p-4 rounded-xl bg-cta/10 text-cta mb-6 group-hover:scale-110 transition-transform duration-300">
<Home className="size-8" />
</div>
<h3 className="text-2xl md:text-3xl font-bold text-primary font-heading mb-4">
Twój Dom
</h3>
<p className="text-text-muted text-base leading-relaxed mb-8 max-w-md">
Instalacje HVAC, prace wykończeniowe, instalacje hydrauliczne oraz kompleksowy serwis sprzętu chłodniczego i AGD.
</p>
</div>
<div className="mt-12 text-center">
<Link href="/uslugi-domowe" className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 mx-auto group">
<span>Pełna oferta usług</span>
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
</div>
</div>
</section>
);
}
+187
View File
@@ -0,0 +1,187 @@
"use client";
import React, { useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Post } from '@/types/types';
import { cn } from '@/lib/utils';
import { Calendar, ArrowRight, ChevronLeft, ChevronRight } from 'lucide-react';
import { FeaturedPost } from '@/components/sections/FeaturedPost';
interface PostsGridProps {
posts: Post[];
featuredPost?: Post;
}
const POSTS_PER_PAGE = 6;
const categories = [
{ id: 'wszystkie', label: 'Wszystkie' },
{ id: 'klimatyzacja', label: 'Klimatyzacja' },
{ id: 'porady', label: 'Porady' },
];
export function PostsGrid({ posts, featuredPost }: PostsGridProps) {
const [activeCategory, setActiveCategory] = useState('wszystkie');
const [currentPage, setCurrentPage] = useState(1);
const filteredPosts = posts.filter(
(post) => activeCategory === 'wszystkie' || post.category === activeCategory
);
const totalPages = Math.ceil(filteredPosts.length / POSTS_PER_PAGE);
const startIndex = (currentPage - 1) * POSTS_PER_PAGE;
const visiblePosts = filteredPosts.slice(startIndex, startIndex + POSTS_PER_PAGE);
const handleCategoryChange = (categoryId: string) => {
setActiveCategory(categoryId);
setCurrentPage(1); // Reset paginacji przy zmianie kategorii
};
const categoryLabels: Record<string, string> = {
'klimatyzacja': 'Klimatyzacja',
'porady': 'Porady'
};
return (
<div className="py-12">
{/* Artykuł Wyróżniony (tylko na 1 stronie) */}
{featuredPost && currentPage === 1 && (
<div className="mb-16">
<h2 className="text-2xl font-bold text-primary font-heading mb-8">Artykuł wyróżniony</h2>
<FeaturedPost post={featuredPost} />
</div>
)}
{/* Filtry */}
<div className="flex flex-wrap items-center gap-3 mb-12">
{categories.map((category) => (
<button
key={category.id}
onClick={() => handleCategoryChange(category.id)}
className={cn(
"px-4 py-2 rounded-full text-sm font-semibold transition-all duration-300 border",
activeCategory === category.id
? "bg-primary text-white border-primary shadow-md"
: "bg-white text-text-muted border-border hover:border-primary/30 hover:text-primary"
)}
>
{category.label}
</button>
))}
</div>
{/* Siatka Artykułów */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{visiblePosts.map((post) => {
const dateObj = new Date(post.date);
const formattedDate = dateObj.toLocaleDateString('pl-PL', {
day: 'numeric', month: 'long', year: 'numeric'
});
return (
<article
key={post.slug}
className="group flex flex-col bg-white rounded-2xl overflow-hidden border border-border hover:shadow-lg transition-all duration-300"
>
<Link href={`/blog/${post.slug}`} className="relative aspect-[16/10] overflow-hidden">
<Image
src={post.image || '/images/hero/hero-section-image.webp'}
alt={post.title}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute top-4 left-4">
<span className="px-3 py-1 bg-accent text-white text-[10px] font-bold rounded-full uppercase tracking-wider shadow-sm">
{categoryLabels[post.category] || post.category}
</span>
</div>
</Link>
<div className="flex flex-col flex-grow p-6 md:p-8">
<div className="flex items-center gap-2 text-xs font-medium text-text-muted mb-3">
<Calendar className="w-3.5 h-3.5 text-primary/50" />
<span>{formattedDate}</span>
</div>
<h3 className="text-xl font-bold text-primary font-heading leading-snug mb-3 group-hover:text-accent transition-colors">
<Link href={`/blog/${post.slug}`}>
{post.title}
</Link>
</h3>
<p className="text-sm text-text-muted leading-relaxed line-clamp-3 mb-6 flex-grow">
{post.excerpt}
</p>
<div className="mt-auto">
<Link
href={`/blog/${post.slug}`}
className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary group-hover:text-accent transition-colors"
>
<span>Czytaj więcej</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
</article>
);
})}
</div>
{visiblePosts.length === 0 && (
<div className="text-center py-16 bg-slate-50 rounded-2xl border border-border">
<p className="text-lg text-text-muted font-medium">Brak artykułów w tej kategorii.</p>
</div>
)}
{/* Paginacja */}
{totalPages > 1 && (
<div className="mt-12 flex flex-wrap justify-center items-center gap-2">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="w-10 h-10 flex items-center justify-center rounded-lg border border-border text-text-muted disabled:opacity-50 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
aria-label="Poprzednia strona"
>
<ChevronLeft className="w-5 h-5" />
</button>
{Array.from({ length: totalPages }).map((_, i) => {
const pageNum = i + 1;
// Prosta logika dla mniejszych ekranów (opcjonalnie można ukrywać strony w środku, ale flex-wrap sobie poradzi)
return (
<button
key={i}
onClick={() => {
setCurrentPage(pageNum);
// Opcjonalnie: skroluj na górę po zmianie strony
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
className={cn(
"w-10 h-10 flex items-center justify-center rounded-lg border transition-all duration-300 font-semibold",
currentPage === pageNum
? "bg-primary text-white border-primary shadow-md"
: "bg-white text-text-muted border-border hover:border-primary/30 hover:text-primary"
)}
>
{pageNum}
</button>
)
})}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="w-10 h-10 flex items-center justify-center rounded-lg border border-border text-text-muted disabled:opacity-50 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
aria-label="Następna strona"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
)}
</div>
);
}
+93
View File
@@ -0,0 +1,93 @@
import React from 'react';
import { pricingData, PricingItem } from '@/content/pricing';
import { Check, Download } from 'lucide-react';
export const PricingTable = () => {
const splitItems = pricingData.filter((item) => item.category === 'split');
const multisplitItems = pricingData.filter((item) => item.category === 'multisplit');
const additionalItems = pricingData.filter((item) => item.category === 'additional');
const renderTableRows = (items: PricingItem[]) => {
return items.map((item) => (
<tr key={item.id} className="border-b border-gray-100 last:border-0 hover:bg-gray-50/50 transition-colors">
<td className="py-4 px-4 text-gray-800 font-medium">{item.name}</td>
<td className="py-4 px-4 text-center whitespace-nowrap text-gray-600">{item.priceNet} zł</td>
<td className="py-4 px-4 text-center whitespace-nowrap font-semibold text-primary">{item.priceGrossCompany} zł</td>
</tr>
));
};
return (
<section className="py-16 bg-white" id="cennik">
<div className="container mx-auto px-4 md:px-8 max-w-5xl">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Cennik usług instalacyjnych</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
Przejrzyste ceny bez ukrytych kosztów. Ostateczna wycena zależy od specyfiki budynku i trudności montażu – dokonywana jest zawsze przed rozpoczęciem prac.
</p>
</div>
<div className="overflow-x-auto rounded-xl border border-gray-200 shadow-sm mb-8">
<table className="w-full text-left border-collapse min-w-[600px]">
<thead>
<tr className="bg-gray-50 border-b border-gray-200 text-sm uppercase text-gray-500">
<th className="py-4 px-4 font-semibold w-1/2">Usługa</th>
<th className="py-4 px-4 font-semibold text-center">Cena netto</th>
<th className="py-4 px-4 font-semibold text-center">Brutto</th>
</tr>
</thead>
<tbody>
{/* Split */}
<tr>
<td colSpan={3} className="bg-gray-50/80 py-3 px-4 font-semibold text-primary text-sm uppercase">
Montaż układów SPLIT
</td>
</tr>
{renderTableRows(splitItems)}
{/* Multisplit */}
<tr>
<td colSpan={3} className="bg-gray-50/80 py-3 px-4 font-semibold text-primary text-sm uppercase">
Montaż układów MULTISPLIT
</td>
</tr>
{renderTableRows(multisplitItems)}
{/* Usługi dodatkowe */}
<tr>
<td colSpan={3} className="bg-gray-50/80 py-3 px-4 font-semibold text-primary text-sm uppercase">
Usługi dodatkowe i serwis
</td>
</tr>
{renderTableRows(additionalItems)}
</tbody>
</table>
</div>
<div className="flex flex-col md:flex-row gap-8 items-start justify-between bg-gray-50 p-6 rounded-xl border border-gray-100">
<div className="flex-1">
<h3 className="font-semibold text-primary mb-3 flex items-center gap-2">
<Check className="w-5 h-5 text-accent" />
Co obejmuje montaż podstawowy?
</h3>
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-gray-600">
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Wykonanie przewiertu przez ścianę</li>
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Instalacja chłodnicza do 3 mb</li>
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Podłączenie do gniazdka (kabel 3m)</li>
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Grawitacyjne odprowadzenie skroplin</li>
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Montaż jednostek do wysokości 3m</li>
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Próba szczelności i uruchomienie</li>
</ul>
</div>
<div className="w-full md:w-64 shrink-0 flex flex-col items-center md:items-stretch gap-4">
<a href="/pdf/cennik-thermcool-2026.pdf" target="_blank" rel="noopener noreferrer" className="btn btn-outline btn-primary bg-white w-fit md:w-full justify-between group flex items-center px-6 py-2 border rounded-lg hover:bg-gray-50 text-sm font-semibold text-primary">
Pobierz cennik PDF
<Download className="w-4 h-4 ml-2 group-hover:-translate-y-1 transition-transform" />
</a>
</div>
</div>
</div>
</section>
);
};
+85
View File
@@ -0,0 +1,85 @@
import {
PhoneCall, ClipboardCheck, Cpu, HeartHandshake, HelpCircle,
ShieldAlert, Construction, Layers, MonitorDown, Server, Gauge, Zap, BookOpen
} from "lucide-react";
import { SectionHeading } from "../common/SectionHeading";
import { mainProcessSteps } from "@/content/process";
import { ProcessStep } from "@/types/types";
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
PhoneCall,
ClipboardCheck,
Cpu,
HeartHandshake,
ShieldAlert,
Construction,
Layers,
MonitorDown,
Server,
Gauge,
Zap,
BookOpen,
};
interface ProcessStepsProps {
steps?: ProcessStep[];
title?: string;
subtitle?: string;
eyebrow?: string;
}
export function ProcessSteps({
steps = mainProcessSteps,
title = "Prosty proces współpracy w 4 krokach",
subtitle = "Od momentu kontaktu do pełnego uruchomienia i serwisu — dbamy o każdy detal na każdym etapie.",
eyebrow = "Jak Działamy"
}: ProcessStepsProps) {
return (
<section className="py-16 md:py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow={eyebrow}
title={title}
subtitle={subtitle}
align="center"
/>
{/* Process Steps Timeline */}
<div className="relative mt-12">
{/* Desktop connecting line (only if few steps, otherwise wrap) */}
<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 (
<div key={idx} className="flex flex-col items-center text-center group">
{/* Circle Badge with Icon & Number */}
<div className="relative size-20 rounded-full bg-slate-50 border-2 border-slate-200 flex items-center justify-center text-primary group-hover:border-accent group-hover:bg-accent-light group-hover:text-accent transition-all duration-300 shadow-sm z-10">
{/* Step Number */}
{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 */}
<h3 className="text-lg font-bold text-primary font-heading mt-6 mb-2">
{step.title}
</h3>
<p className="text-sm text-text-muted leading-relaxed max-w-[240px]">
{step.description}
</p>
</div>
);
})}
</div>
</div>
</div>
</section>
);
}
+54
View File
@@ -0,0 +1,54 @@
import React from 'react';
import Image from 'next/image';
import { projectsData, Project } from '@/content/projects';
interface ProjectsGalleryProps {
category: Project['category'];
title?: string;
description?: string;
}
export const ProjectsGallery = ({
category,
title = "Nasze realizacje",
description = "Zobacz przykłady prac wykonanych przez naszych certyfikowanych instalatorów."
}: ProjectsGalleryProps) => {
const projects = projectsData.filter((project) => project.category === category);
if (projects.length === 0) return null;
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
<div className="text-center max-w-2xl mx-auto mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">{title}</h2>
<p className="text-gray-600">{description}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{projects.map((project) => (
<div key={project.id} className="group rounded-2xl overflow-hidden border border-gray-100 shadow-sm hover:shadow-lg transition-all duration-300 bg-white flex flex-col h-full">
<div className="relative h-64 overflow-hidden bg-gray-100">
<Image
src={project.imageUrl}
alt={project.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-500"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
<div className="p-6 flex-grow flex flex-col">
<h3 className="font-bold text-lg text-primary mb-2 group-hover:text-accent transition-colors">
{project.title}
</h3>
<p className="text-gray-600 text-sm mb-4 flex-grow">
{project.description}
</p>
</div>
</div>
))}
</div>
</div>
</section>
);
};
+95
View File
@@ -0,0 +1,95 @@
"use client";
import React, { useState } from 'react';
import Image from 'next/image';
import { projectsData, Project } from '@/content/projects';
import { cn } from '@/lib/utils';
import { MapPin } from 'lucide-react';
type Category = 'wszystkie' | Project['category'];
const categories: { id: Category; label: string }[] = [
{ id: 'wszystkie', label: 'Wszystkie' },
{ id: 'klimatyzacja', label: 'Klimatyzacja' },
];
export function ProjectsGrid() {
const [activeCategory, setActiveCategory] = useState<Category>('wszystkie');
const filteredProjects = projectsData.filter(
(project) => activeCategory === 'wszystkie' || project.category === activeCategory
);
return (
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Filtry */}
<div className="flex flex-wrap items-center justify-center gap-3 mb-12">
{categories.map((category) => (
<button
key={category.id}
onClick={() => setActiveCategory(category.id)}
className={cn(
"px-5 py-2.5 rounded-full text-sm font-semibold transition-all duration-300 border",
activeCategory === category.id
? "bg-primary text-white border-primary shadow-md"
: "bg-white text-text-muted border-border hover:border-primary/30 hover:text-primary"
)}
>
{category.label}
</button>
))}
</div>
{/* Siatka */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredProjects.map((project) => (
<div
key={project.id}
className="group relative rounded-2xl overflow-hidden bg-slate-100 aspect-[4/3]"
>
<Image
src={project.imageUrl}
alt={project.title}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
{/* Nakładka gradientowa */}
<div className="absolute inset-0 bg-gradient-to-t from-primary/90 via-primary/40 to-transparent opacity-80 group-hover:opacity-90 transition-opacity duration-300" />
{/* Treść (Zawsze widoczna lokalizacja i kategoria, pojawiający się opis) */}
<div className="absolute inset-0 p-6 flex flex-col justify-end">
<div className="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
<div className="flex items-center gap-3 mb-3">
<span className="px-3 py-1 bg-accent text-white text-xs font-bold rounded-full uppercase tracking-wider">
{categories.find(c => c.id === project.category)?.label}
</span>
<div className="flex items-center gap-1 text-white/80 text-sm font-medium">
<MapPin className="w-3.5 h-3.5" />
<span>{project.location}</span>
</div>
</div>
<h3 className="text-xl font-bold text-white mb-2 font-heading">
{project.title}
</h3>
<p className="text-white/80 text-sm line-clamp-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 delay-100">
{project.description}
</p>
</div>
</div>
</div>
))}
</div>
{filteredProjects.length === 0 && (
<div className="text-center py-20 text-text-muted">
<p className="text-lg">Brak realizacji w tej kategorii.</p>
</div>
)}
</section>
);
}
+47
View File
@@ -0,0 +1,47 @@
import { SectionHeading } from "../common/SectionHeading";
import { IconCard } from "../common/IconCard";
import { services as defaultServices } from "@/content/services";
interface ServicesGridProps {
services?: {
title: string;
description: string;
icon: string;
href?: string;
}[];
title?: string;
subtitle?: string;
eyebrow?: string;
}
export function ServicesGrid({
services = defaultServices,
title = "Kompleksowe usługi dla Twojego domu",
subtitle = "Zapewniamy profesjonalne wsparcie w zakresie instalacji sanitarnych, grzewczych, chłodniczych oraz prac wykończeniowych.",
eyebrow = "Pełna Oferta"
}: ServicesGridProps) {
return (
<section className="py-16 md:py-24 bg-slate-50 border-t border-b border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow={eyebrow}
title={title}
subtitle={subtitle}
align="center"
/>
<div className={`grid grid-cols-1 sm:grid-cols-2 gap-6 md:gap-8 mt-10 ${services.length === 2 ? 'max-w-4xl mx-auto' : 'lg:grid-cols-3'}`}>
{services.map((service, idx) => (
<IconCard
key={idx}
title={service.title}
description={service.description}
iconName={service.icon}
href={service.href}
/>
))}
</div>
</div>
</section>
);
}
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import { companyDetails } from '@/content/company';
export function StatsBar() {
return (
<section className="bg-primary text-white py-12 md:py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-4 divide-x divide-white/10">
{companyDetails.stats.map((stat, idx) => (
<div key={idx} className="flex flex-col items-center justify-center text-center px-4">
<div className="text-4xl md:text-5xl font-extrabold text-accent mb-2 font-heading">
{stat.value}
</div>
<div className="text-sm md:text-base font-medium text-white/80 uppercase tracking-wider">
{stat.label}
</div>
</div>
))}
</div>
</div>
</section>
);
}
+64
View File
@@ -0,0 +1,64 @@
import React from 'react';
import { ThermometerSnowflake, VolumeX, Droplets, Snowflake, PowerOff } from 'lucide-react';
const symptoms = [
{
icon: ThermometerSnowflake,
title: 'Lodówka nie chłodzi',
description: 'Najczęstsza usterka. Może oznaczać problem z termostatem, wyciek czynnika chłodniczego lub uszkodzenie sprężarki.',
},
{
icon: VolumeX,
title: 'Głośna praca / Dziwne dźwięki',
description: 'Buczenie, stukanie lub piszczenie często wskazuje na zużycie wentylatora, agregatu lub problem z ułożeniem urządzenia.',
},
{
icon: Droplets,
title: 'Zbieranie się wody na dnie',
description: 'Zazwyczaj spowodowane zatkanym odpływem skroplin w chłodziarce lub problemem z systemem No Frost.',
},
{
icon: Snowflake,
title: 'Nadmierne oszranianie',
description: 'Brak uszczelnienia drzwi, uszkodzona grzałka odszraniania lub awaria czujnika temperatury.',
},
{
icon: PowerOff,
title: 'Urządzenie nie włącza się',
description: 'Problemy z zasilaniem, uszkodzony moduł sterujący (płyta główna) lub całkowita awaria agregatu.',
},
];
export const SymptomsList = () => {
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-6xl">
<div className="text-center max-w-2xl mx-auto mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Typowe usterki — kiedy wezwać serwis?</h2>
<p className="text-gray-600">
Jeśli zauważysz którykolwiek z poniższych objawów, nie zwlekaj. Szybka reakcja może zapobiec poważniejszej awarii i uchronić żywność przed zepsuciem.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{symptoms.map((symptom, idx) => {
const Icon = symptom.icon;
return (
<div key={idx} className="bg-gray-50 p-6 rounded-xl border border-gray-100 flex gap-4 items-start hover:shadow-md transition-shadow">
<div className="w-12 h-12 shrink-0 bg-accent/10 rounded-lg flex items-center justify-center text-accent mt-1">
<Icon className="w-6 h-6" />
</div>
<div>
<h3 className="font-bold text-primary mb-2">{symptom.title}</h3>
<p className="text-gray-600 text-sm leading-relaxed">
{symptom.description}
</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
+78
View File
@@ -0,0 +1,78 @@
'use client';
import useEmblaCarousel from 'embla-carousel-react';
import Autoplay from 'embla-carousel-autoplay';
import { Star, ChevronLeft, ChevronRight } from 'lucide-react';
import { useCallback } from 'react';
const testimonials = [
{ name: 'Anna Kowalska', city: 'Wrocław', rating: 5, text: 'Montaż przeprowadzony sprawnie i bez bałaganu. Ekipa przybyła punktualnie, wszystko wytłumaczyli i zostawili po sobie idealny porządek. Klimatyzator działa cicho i skutecznie.' },
{ name: 'Marek Wiśniewski', city: 'Legnica', rating: 5, text: 'Polecam z całego serca. Fachowa obsługa, dobra cena i bardzo miły kontakt. Klimatyzator dobrali idealnie do mojego salonu 40 m². Już pierwszego lata oszczędności widoczne na rachunkach.' },
{ name: 'Katarzyna Nowak', city: 'Wrocław', rating: 5, text: 'Bardzo profesjonalna firma. Zamówiłam klimatyzację do sypialni i biura — dwie jednostki zamontowane w jeden dzień. Instalacja ukryta w brudzie, wygląda świetnie.' },
{ name: 'Piotr Zając', city: 'Wałbrzych', rating: 5, text: 'Szybka wycena, konkretne podejście bez owijania w bawełnę. Klimatyzator Gree działa już drugi sezon bez żadnych problemów. Serwis też na wysokim poziomie.' },
{ name: 'Monika Dąbrowska', city: 'Jelenia Góra', rating: 5, text: 'Świetna obsługa od pierwszego kontaktu do odbioru prac. Montażyści zadbali o każdy detal, przeprowadzenie rur estetyczne. Klimatyzacja grzeje też zimą — duże zaskoczenie!' },
{ name: 'Tomasz Lewandowski', city: 'Wrocław', rating: 5, text: 'Bardzo dobra firma. Mam multisplit na 3 pokoje — wszystko działa bez zarzutu. Cena montażu zgodna z wyceną, bez ukrytych kosztów. Zdecydowanie wrócę przy kolejnej inwestycji.' },
{ name: 'Agnieszka Wróbel', city: 'Oleśnica', rating: 5, text: 'Polecam każdemu. Szybko, czysto i profesjonalnie. Klimatyzator marki Rotenso — cichy jak myszka, a chłodzi rewelacyjnie. Wreszcie sypialnia w której można spać latem.' },
{ name: 'Krzysztof Jabłoński', city: 'Lubin', rating: 5, text: 'Solidna robota. Montaż kasetonowy w biurze 60 m² — ekipa wiedziała co robi, nie tracili czasu. Urządzenie działa cicho, pracownicy zadowoleni. Na pewno polecę znajomym.' },
{ name: 'Ewa Kamińska', city: 'Wrocław', rating: 5, text: 'Doskonały kontakt i profesjonalizm na każdym etapie. Doradca pomógł wybrać odpowiednią moc, montaż odbył się następnego dnia. Klimatyzacja w domu to teraz standard.' },
{ name: 'Radosław Szymański', city: 'Świdnica', rating: 5, text: 'Bardzo sprawna realizacja. Zamówiłem w środę, w piątek już miałem zamontowaną klimatyzację. Jakość wykonania na wysokim poziomie, wszystko dokładnie wyjaśnione.' },
{ name: 'Marta Kowalczyk', city: 'Oława', rating: 5, text: 'Super firma! Klimatyzator Samsung dobrana idealnie do naszego mieszkania. Montażyści schludni, uprzejmi i szybcy. Instalacja w korytku wygląda bardzo estetycznie.' },
{ name: 'Grzegorz Woźniak', city: 'Wrocław', rating: 5, text: 'Drugi raz korzystam z usług ThermCool i znowu jestem w pełni zadowolony. Tym razem pompa ciepła — ekipa poradziła sobie z trudnym miejscem montażu bez problemu.' },
{ name: 'Joanna Michalska', city: 'Trzebnica', rating: 5, text: 'Bardzo dobra obsługa i uczciwe ceny. Klimatyzacja Daikin pracuje bezbłędnie od ponad roku. Przy okazji montażu doradca wyjaśnił jak dbać o urządzenie żeby służyło jak najdłużej.' },
{ name: 'Bartosz Krawczyk', city: 'Głogów', rating: 5, text: 'Polecam! Montaż multisplit do domu jednorodzinnego — 4 jednostki wewnętrzne zamontowane w dwa dni. Praca czysta, instalacja ukryta w brudzie. Efekt końcowy powyżej oczekiwań.' },
{ name: 'Sylwia Pawlak', city: 'Środa Śląska', rating: 5, text: 'Klimatyzator Haier dobrany pod kątem rozmiarów pokoju i budżetu. Bardzo pomocny doradca, bez zbędnego wciskania droższych modeli. Montaż szybki i bez bałaganu. Polecam!' },
];
export function Testimonials() {
const [emblaRef, emblaApi] = useEmblaCarousel(
{ loop: true, align: 'start', slidesToScroll: 1 },
[Autoplay({ delay: 4000, stopOnInteraction: true })]
);
const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
return (
<section className="py-16 bg-slate-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between mb-10">
<div>
<h2 className="text-2xl md:text-3xl font-extrabold text-primary font-heading">
Co mówią nasi klienci
</h2>
<p className="text-text-muted mt-1">Opinie klientów</p>
</div>
<div className="flex gap-2">
<button onClick={scrollPrev} aria-label="Poprzednia opinia"
className="size-10 rounded-xl border border-border bg-white flex items-center justify-center text-primary hover:border-accent hover:text-accent transition-colors">
<ChevronLeft className="size-5" />
</button>
<button onClick={scrollNext} aria-label="Następna opinia"
className="size-10 rounded-xl border border-border bg-white flex items-center justify-center text-primary hover:border-accent hover:text-accent transition-colors">
<ChevronRight className="size-5" />
</button>
</div>
</div>
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex gap-5">
{testimonials.map((t, i) => (
<div key={i} className="flex-none w-[320px] md:w-[360px] bg-white rounded-2xl border border-border p-6 flex flex-col gap-3">
<div className="flex gap-0.5">
{Array.from({ length: t.rating }).map((_, j) => (
<Star key={j} className="size-4 fill-yellow-400 text-yellow-400" />
))}
</div>
<p className="text-sm text-text-muted leading-relaxed flex-1">&quot;{t.text}&quot;</p>
<div className="pt-3 border-t border-border">
<p className="text-sm font-bold text-primary">{t.name}</p>
<p className="text-xs text-text-muted">{t.city}</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
);
}
+54
View File
@@ -0,0 +1,54 @@
import React from 'react';
import { Home, Zap, Wrench } from 'lucide-react';
const cases = [
{
icon: Home,
title: 'Nowo budowany dom',
description: 'Najlepszy moment na decyzję o pompie ciepła. Pozwala to na uniknięcie budowy komina, przyłącza gazowego i wydzielania kotłowni. Idealnie współpracuje z nowoczesnym ogrzewaniem podłogowym.',
},
{
icon: Wrench,
title: 'Wymiana starego kotła (kopciucha)',
description: 'Zastąpienie uciążliwego pieca węglowego bezobsługową pompą. Często wymaga dostosowania istniejącej instalacji, ale daje dostęp do najwyższych poziomów dofinansowania z programu Czyste Powietrze.',
},
{
icon: Zap,
title: 'Modernizacja i hybryda',
description: 'Dołożenie pompy ciepła do istniejącego, wciąż sprawnego kotła gazowego lub olejowego. Pompa przejmuje ogrzewanie przez większość sezonu, a stary kocioł wspomaga system tylko w największe mrozy.',
},
];
export const UseCases = () => {
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-5xl">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Kiedy warto zainwestować?</h2>
<p className="text-gray-600">
Pompa ciepła to elastyczne rozwiązanie, które sprawdzi się w różnych scenariuszach życiowych i inwestycyjnych.
</p>
</div>
<div className="space-y-6">
{cases.map((useCase, idx) => {
const Icon = useCase.icon;
return (
<div key={idx} className="flex flex-col sm:flex-row gap-6 p-6 md:p-8 rounded-2xl border border-gray-100 hover:border-accent/30 transition-colors bg-gray-50/50 hover:bg-white group">
<div className="w-16 h-16 shrink-0 bg-white border border-gray-100 shadow-sm rounded-2xl flex items-center justify-center text-primary group-hover:text-accent group-hover:border-accent/20 transition-all">
<Icon className="w-8 h-8" />
</div>
<div>
<h3 className="text-xl font-bold text-primary mb-3">{useCase.title}</h3>
<p className="text-gray-600 leading-relaxed">
{useCase.description}
</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
+56
View File
@@ -0,0 +1,56 @@
import React from 'react';
import { SectionHeading } from '../common/SectionHeading';
import { ShieldCheck, Clock, ThumbsUp, HeartHandshake } from 'lucide-react';
const values = [
{
title: 'Najwyższa jakość',
description: 'Nie idziemy na kompromisy. Stosujemy wyłącznie sprawdzone materiały i urządzenia renomowanych producentów.',
icon: ThumbsUp
},
{
title: 'Terminowość',
description: 'Szanujemy Twój czas. Każde zlecenie realizujemy zgodnie z ustalonym harmonogramem.',
icon: Clock
},
{
title: 'Bezpieczeństwo',
description: 'Posiadamy wszystkie niezbędne uprawnienia (m.in. F-Gaz, SEP) oraz pełne ubezpieczenie OC firmy.',
icon: ShieldCheck
},
{
title: 'Długofalowe relacje',
description: 'Zależy nam na zaufaniu. Po skończonym montażu czy remoncie oferujemy pełną opiekę serwisową.',
icon: HeartHandshake
}
];
export function ValuesGrid() {
return (
<section className="py-16 md:py-24 bg-slate-50 border-y border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<SectionHeading
eyebrow="Dlaczego my"
title="Nasze wartości"
subtitle="Zasady, którymi kierujemy się w codziennej pracy z naszymi klientami."
align="center"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 mt-16">
{values.map((val, idx) => {
const Icon = val.icon;
return (
<div key={idx} className="bg-white p-8 rounded-2xl border border-border text-center hover:shadow-lg transition-shadow duration-300">
<div className="mx-auto w-16 h-16 bg-accent/10 text-accent rounded-2xl flex items-center justify-center mb-6 transform -rotate-3 hover:rotate-0 transition-transform">
<Icon className="w-8 h-8" />
</div>
<h3 className="text-xl font-bold text-primary font-heading mb-3">{val.title}</h3>
<p className="text-text-muted leading-relaxed text-sm">{val.description}</p>
</div>
);
})}
</div>
</div>
</section>
);
}
+86
View File
@@ -0,0 +1,86 @@
import Image from "next/image";
import { Award, FileText, ShieldCheck, Sparkles } from "lucide-react";
import { companyDetails } from "@/content/company";
export function WhyUs() {
const advantages = [
{
icon: Award,
title: "Doświadczenie i certyfikaty",
description: "Posiadamy wszystkie wymagane certyfikaty i autoryzacje. Działamy na rynku od ponad 15 lat.",
},
{
icon: FileText,
title: "Jasna wycena bez ukrytych kosztów",
description: "Przed montażem otrzymujesz pełny kosztorys. Ceny podane w ofercie są cenami ostatecznymi.",
},
{
icon: ShieldCheck,
title: "Gwarancja i wsparcie serwisowe",
description: "Zapewniamy pełną opiekę pogwarancyjną oraz coroczne przeglądy zachowujące gwarancję producenta.",
},
{
icon: Sparkles,
title: "Czystość i dbałość o detale",
description: "Pracujemy z użyciem odkurzaczy przemysłowych. Po montażu zostawiamy pomieszczenia idealnie czyste.",
},
];
return (
<section className="py-16 md:py-24 bg-slate-50 border-t border-b border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-center">
{/* Left Column: Title, Intro and Statistics */}
<div className="lg:col-span-5 flex flex-col gap-6">
<div>
<span className="text-xs md:text-sm font-semibold uppercase tracking-wider text-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
</h2>
<p className="text-sm md:text-base text-text-muted mt-4 leading-relaxed">
Stawiamy na najwyższą jakość wykonania, terminowość oraz bezkompromisowe bezpieczeństwo. Pomagamy na każdym kroku — od wyboru odpowiedniego urządzenia, poprzez załatwienie dotacji, aż po finalny montaż i regularny serwis.
</p>
<div className="relative w-full h-64 rounded-2xl overflow-hidden mt-6">
<Image
src="/images/services/wycena.webp"
alt="Bezpłatna wycena klimatyzacji ThermCool"
fill
className="object-cover"
/>
</div>
</div>
</div>
{/* Right Column: Advantages Grid */}
<div className="lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-6">
{advantages.map((adv, idx) => {
const Icon = adv.icon;
return (
<div
key={idx}
className="p-6 rounded-xl bg-white border border-border hover:shadow-md transition-shadow flex flex-col gap-4"
>
<div className="size-12 rounded-lg bg-accent-light text-accent flex items-center justify-center shrink-0">
<Icon className="size-6" />
</div>
<div>
<h3 className="text-base font-bold text-primary font-heading">
{adv.title}
</h3>
<p className="text-xs md:text-sm text-text-muted mt-1.5 leading-relaxed">
{adv.description}
</p>
</div>
</div>
);
})}
</div>
</div>
</div>
</section>
);
}